Eric Workman

Parsing JSON With Elm

Published on

I'm diving into Elm. I recently added an Elm frontend to Montovat and started serving the stored articles from a JSON api. I wasn't able to find a very good explanation on how to use the JSON Decoder library, so I thought I'd make a post about it. The goal here is to wrap an existing api endpoint with an Elm interface enough to show how it all works.

I'm using Elm 0.18.0 and the cli tools, but this guide will work with Try Elm too.

The Data

I'll be using the JSONPlaceholder's users endpoint for this, because it is easy and readily available. The data this endpoint returns looks like

[{
  id: 1,
  name: "Leanne Graham",
  username: "Bret",
  email: "Sincere@april.biz",
  address: {
    street: "Kulas Light",
    suite: "Apt. 556",
    city: "Gwenborough",
    zipcode: "92998-3874",
    geo: {
      lat: "-37.3159",
      lng: "81.1496"
    }
  },
  phone: "1-770-736-8031 x56442",
  website: "hildegard.org",
  company: {
    name: "Romaguera-Crona",
    catchPhrase: "Multi-layered client-server neural-net",
    bs: "harness real-time e-markets"
  }
},
{...}]

Looks straight forward: a list of objects (Users) with embedded Company object and Address object with a further embedded Geo object. A quick glance through the data reveals that there are no optional fields, the types of each key value pair match the other objects', and the containing structure does not have a key (the data isn't in the form {users: [{}, {}, ...]}).

Modeling the Data

Starting with the deepest embedded object we'll creatively call Geo, we can start to map out the types:

type alias Geo =
  { lat: String
  , lng: String
  }

The decoder for Geo is a two-field map:

-- I'll leave off this import from the rest of the objects
import Json.Decode as Decode

geoDecoder : Decode.Decoder Geo
geoDecoder =
  Decode.map2 Geo
    (Decode.field "lat" Decode.string)
    (Decode.field "lng" Decode.string)

Company quickly follows:

type alias Company =
  { bs: String
  , catchPhrase: String
  , name: String
  }

companyDecoder : Decode.Decoder Company
companyDecoder =
  Decode.map3 Company
    (Decode.field "bs" Decode.string)
    (Decode.field "catchPhrase" Decode.string)
    (Decode.field "name" Decode.string)

And we are out of the simple objects. Address only has Geo embedded, so we'll start there:

type alias Address =
  { city: String
  , geo: Geo
  , street: String
  , suite: String
  , zipcode: String
  }

The Address type embeds the Geo type. The Address decoder can also embed the Geo decoder:

addressDecoder : Decode.Decoder Address
addressDecoder =
  Decode.map5 Address
    (Decode.field "city" Decode.string)
    (Decode.field "geo" geoDecoder)
    (Decode.field "street" Decode.string)
    (Decode.field "suite" Decode.string)
    (Decode.field "zipcode" Decode.string)

Time to tackle User:

type alias User =
  { address: Address
  , company: Company
  , email: String
  , id: Int
  , name: String
  , phone: String
  , username: String
  , website: String
  }

userDecoder : Decode.Decoder User
userDecoder =
  Decode.map8 User
    (Decode.field "address" addressDecoder)
    (Decode.field "company" companyDecoder)
    (Decode.field "email" Decode.string)
    (Decode.field "id" Decode.int)
    (Decode.field "name" Decode.string)
    (Decode.field "phone" Decode.string)
    (Decode.field "username" Decode.string)
    (Decode.field "website" Decode.string)

With eight fields, Decode.map8 still has us covered. If User had 9 fields, we'd have to get into elm-decode-pipeline. We will in the future, but we can avoid it for now.

The last object is the structure containing the list of Users. We'll call it the UserContainer, but as I called out earlier, the data is missing any kind of key for a field. We'll use one in the type alias. We'll have to decode it a little differently than the others:

type alias UserContainer =
  { users: List User }

userContainerDecoder : Decode.Decoder UserContainer
userContainerDecoder =
  Decode.map UserContainer
    -- No Decode.field here
    (Decode.list userDecoder)

Pulling in the Data

We've modeled the data, and now we need to use the decoders when we fetch the data:

import Http

api : String
api =
  "https://jsonplaceholder.typicode.com/users"

fetchUsers : String -> Http.Request UserContainer
fetchUsers jsonApi =
  Http.get jsonApi userContainerDecoder

Using the Data

Add on a program that outputs a little html, and

import Html exposing (..)

type Msg
  = GotUsers (Result Http.Error UserContainer)
  | FetchUsers

update : Msg -> UserContainer -> (UserContainer, Cmd Msg)
update msg model =
  case msg of
    FetchUsers ->
      ( model, Http.send GotUsers (fetchUsers api) )
    GotUsers result ->
      case result of
        Err httpError ->
          let
            _ =
              Debug.log "handleUsersError" httpError
          in
            ( model, Cmd.none )
        Ok userContainer ->
          ( userContainer, Cmd.none )

view : UserContainer -> Html Msg
view model =
  div [] [h2 [] [ text "Users"]
  , div [] (List.map (renderUser) model.users)
  ]

renderUser : User -> Html Msg
renderUser model =
  div [] [ h3 [] [ text (model.name ++ " (" ++ model.username ++ ")") ]
  , p [] [ text model.email ]
  -- Embedded resources are available through the dot notation
  , p [] [ text model.address.city ]
  , p [] [ text model.company.name ]
  ]

init : ( UserContainer, Cmd Msg )
init =
  update FetchUsers
    { users = [] }

main : Program Never UserContainer Msg
main =
  Html.program
    { init = init
    , subscriptions = always Sub.none
    , view = view
    , update = update
    }

Results

We are done! Run the app with elm-reactor and see the results

elm results

elm results

In a future post, I'll cover elm-decode-pipeline, optional fields, and fields with varying datatypes. Here's the full application that's pastable into Try Elm.