JSON

We just saw an example that uses HTTP to get the content of a book. That is great, but a ton of servers return data in a special format called JavaScript Object Notation, or JSON for short.

So our next example shows how to fetch some JSON data, allowing us to press a button to show random cat GIFs. Click the blue “Edit” button and look through the program a bit. Try not to only look at the cats! Click the blue button now!

Edit

  1. import Browser
  2. import Html exposing (..)
  3. import Html.Attributes exposing (..)
  4. import Html.Events exposing (..)
  5. import Http
  6. import Json.Decode exposing (Decoder, field, string)
  7. -- MAIN
  8. main =
  9. Browser.element
  10. { init = init
  11. , update = update
  12. , subscriptions = subscriptions
  13. , view = view
  14. }
  15. -- MODEL
  16. type Model
  17. = Failure
  18. | Loading
  19. | Success String
  20. init : () -> (Model, Cmd Msg)
  21. init _ =
  22. (Loading, getRandomCatGif)
  23. -- UPDATE
  24. type Msg
  25. = MorePlease
  26. | GotGif (Result Http.Error String)
  27. update : Msg -> Model -> (Model, Cmd Msg)
  28. update msg model =
  29. case msg of
  30. MorePlease ->
  31. (Loading, getRandomCatGif)
  32. GotGif result ->
  33. case result of
  34. Ok url ->
  35. (Success url, Cmd.none)
  36. Err _ ->
  37. (Failure, Cmd.none)
  38. -- SUBSCRIPTIONS
  39. subscriptions : Model -> Sub Msg
  40. subscriptions model =
  41. Sub.none
  42. -- VIEW
  43. view : Model -> Html Msg
  44. view model =
  45. div []
  46. [ h2 [] [ text "Random Cats" ]
  47. , viewGif model
  48. ]
  49. viewGif : Model -> Html Msg
  50. viewGif model =
  51. case model of
  52. Failure ->
  53. div []
  54. [ text "I could not load a random cat for some reason. "
  55. , button [ onClick MorePlease ] [ text "Try Again!" ]
  56. ]
  57. Loading ->
  58. text "Loading..."
  59. Success url ->
  60. div []
  61. [ button [ onClick MorePlease, style "display" "block" ] [ text "More Please!" ]
  62. , img [ src url ] []
  63. ]
  64. -- HTTP
  65. getRandomCatGif : Cmd Msg
  66. getRandomCatGif =
  67. Http.get
  68. { url = "https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=cat"
  69. , expect = Http.expectJson GotGif gifDecoder
  70. }
  71. gifDecoder : Decoder String
  72. gifDecoder =
  73. field "data" (field "image_url" string)

This example is pretty similar to the last one:

  • init starts us off in the Loading state, with a command to get a random cat GIF.
  • update handles the GotGif message for whenever a new GIF is available. Whatever happens there, we do not have any additional commands. It also handles the MorePlease message when someone presses the button, issuing a command to get more random cats.
  • view shows you the cats!

The main difference is in the getRandomCatGif definition. Instead of using Http.expectString, we have switched to Http.expectJson. What is the deal with that?

JSON

When you ask api.giphy.com for a random cat GIF, their server produces a big string of JSON like this:

  1. {
  2. "data": {
  3. "type": "gif",
  4. "id": "l2JhxfHWMBWuDMIpi",
  5. "title": "cat love GIF by The Secret Life Of Pets",
  6. "image_url": "https://media1.giphy.com/media/l2JhxfHWMBWuDMIpi/giphy.gif",
  7. "caption": "",
  8. ...
  9. },
  10. "meta": {
  11. "status": 200,
  12. "msg": "OK",
  13. "response_id": "5b105e44316d3571456c18b3"
  14. }
  15. }

We have no guarantees about any of the information here. The server can change the names of fields, and the fields may have different types in different situations. It is a wild world!

In JavaScript, the approach is to just turn JSON into JavaScript objects and hope nothing goes wrong. But if there is some typo or unexpected data, you get a runtime exception somewhere in your code. Was the code wrong? Was the data wrong? It is time to start digging around to find out!

In Elm, we validate the JSON before it comes into our program. So if the data has an unexpected structure, we learn about it immediately. There is no way for bad data to sneak through and cause a runtime exception three files over. This is accomplished with JSON decoders.

JSON Decoders

Say we have some JSON:

  1. {
  2. "name": "Tom",
  3. "age": 42
  4. }

We need to run it through a Decoder to access specific information. So if we wanted to get the "age", we would run the JSON through a Decoder Int that describes exactly how to access that information:

JSON - 图1

If all goes well, we get an Int on the other side! And if we wanted the "name" we would run the JSON through a Decoder String that describes exactly how to access it:

JSON - 图2

If all goes well, we get a String on the other side!

How do we create decoders like this though?

Building Blocks

The elm/json package gives us the Json.Decode module. It is filled with tiny decoders that we can snap together.

So to get "age" from { "name": "Tom", "age": 42 } we would create a decoder like this:

  1. import Json.Decode exposing (Decoder, field, int)
  2. ageDecoder : Decoder Int
  3. ageDecoder =
  4. field "age" int
  5. -- int : Decoder Int
  6. -- field : String -> Decoder a -> Decoder a

The field function takes two arguments:

  1. String — a field name. So we are demanding an object with an "age" field.
  2. Decoder a — a decoder to try next. So if the "age" field exists, we will try this decoder on the value there.

So putting it together, field "age" int is asking for an "age" field, and if it exists, it runs the Decoder Int to try to extract an integer.

We do pretty much exactly the same thing to extract the "name" field:

  1. import Json.Decode exposing (Decoder, field, string)
  2. nameDecoder : Decoder String
  3. nameDecoder =
  4. field "name" string
  5. -- string : Decoder String

In this case we demand an object with a "name" field, and if it exists, we want the value there to be a String.

Nesting Decoders

Remember the api.giphy.com data?

  1. {
  2. "data": {
  3. "type": "gif",
  4. "id": "l2JhxfHWMBWuDMIpi",
  5. "title": "cat love GIF by The Secret Life Of Pets",
  6. "image_url": "https://media1.giphy.com/media/l2JhxfHWMBWuDMIpi/giphy.gif",
  7. "caption": "",
  8. ...
  9. },
  10. "meta": {
  11. "status": 200,
  12. "msg": "OK",
  13. "response_id": "5b105e44316d3571456c18b3"
  14. }
  15. }

We wanted to access response.data.image_url to show a random GIF. Well, we have the tools now!

  1. import Json.Decode exposing (Decoder, field, string)
  2. gifDecoder : Decoder String
  3. gifDecoder =
  4. field "data" (field "image_url" string)

This is the exact gifDecoder definition we used in our example program above! Is there a "data" field? Does that value have an "image_url" field? Is the value there a string? All our expectations are written out explicitly, allowing us to safely extract Elm values from JSON.

Combining Decoders

That is all we needed for our HTTP example, but decoders can do more! For example, what if we want two fields? We snap decoders together with map2:

  1. map2 : (a -> b -> value) -> Decoder a -> Decoder b -> Decoder value

This function takes in two decoders. It tries them both and combines their results. So now we can put together two different decoders:

  1. import Json.Decode exposing (Decoder, map2, field, string, int)
  2. type alias Person =
  3. { name : String
  4. , age : Int
  5. }
  6. personDecoder : Decoder Person
  7. personDecoder =
  8. map2 Person
  9. (field "name" string)
  10. (field "age" int)

So if we used personDecoder on { "name": "Tom", "age": 42 } we would get out an Elm value like Person "Tom" 42.

If we really wanted to get into the spirit of decoders, we would define personDecoder as map2 Person nameDecoder ageDecoder using our previous definitions. You always want to be building your decoders up from smaller building blocks!

Next Steps

There are a bunch of important functions in Json.Decode that we did not cover here:

  • bool : Decoder Bool
  • list : Decoder a -> Decoder (List a)
  • dict : Decoder a -> Decoder (Dict String a)
  • oneOf : List (Decoder a) -> Decoder a

So there are ways to extract all sorts of data structures. The oneOf function is particularly helpful for messy JSON. (e.g. sometimes you get an Int and other times you get a String containing digits. So annoying!)

There are also map3, map4, and others for handling objects with more than two fields. But as you start working with larger JSON objects, it is worth checking out NoRedInk/elm-json-decode-pipeline. The types there are a bit fancier, but some folks find them much easier to read and work with.

Fun Fact: I have heard a bunch of stories of folks finding bugs in their server code as they switched from JS to Elm. The decoders people write end up working as a validation phase, catching weird stuff in JSON values. So when NoRedInk switched from React to Elm, it revealed a couple bugs in their Ruby code!