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 (User
s) 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 User
s. 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
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.