Suppose you have some JSON that describes map markers:
{
"name": "Rixos The Palm Dubai",
"lat": 25.1212,
"lon": 55.1535
}
Using Json.Decode
, the decoder for this JSON looks like this:
decodeMarker : Json.Decode.Decoder Marker
decodeMarker =
Json.Decode.map3 Marker
(field "name" Json.Decode.string)
(field "lat" Json.Decode.float)
(field "lon" Json.Decode.float)
If your JSON acquires another field (say, marker color), you need to add one more field decoder ((field "color" Json.Decode.string)
) and change map3
to map4
in the above decoder. But this can only extend so far as Json.Decode
map
functions only go up to map8
.
What if your JSON keeps accreting fields such that you end up with more than 8 fields? At this point, you could bust out Json.Decode.Pipeline
which doesn’t have this limitation. However, it’s possible to decode a larger number of fields by combining the Json.Decode.map*
calls as well.
Consider this flat JSON object with a larger number of fields (let’s pretend there are 10 fields in total):
{
"type": "marker",
"name": "Rixos The Palm Dubai",
"description": "Hotel",
"color": "red",
...
"lat": 25.1212,
"lon": 55.1535
}
The decoder for this function can be broken up into two pieces
decodeMarker =
let
mainFields =
Json.Decode.map8 Marker
(field "type" Json.Decode.string)
(field "name" Json.Decode.string)
(field "description" Json.Decode.string)
(field "color" Json.Decode.string)
...
in
Json.Decode.map3
(<|)
mainFields
(field "lat" Json.Decode.float)
(field "lon" Json.Decode.float)
Note that in the call to map3
, instead of the record constructor, we have to use the function application operator, and then mainFields
is followed by decoders for the remaining fields
It’s also possible to break up the fields into more than two groups using this approach:
decodeMarker =
let
mainFields =
Json.Decode.map2 Marker
(field "type" Json.Decode.string)
(field "name" Json.Decode.string)
extraFields =
Json.Decode.map3 (<|)
mainFields
(field "description" Json.Decode.string)
(field "color" Json.Decode.string)
in
Json.Decode.map3 (<|)
extraFields
(field "lat" Json.Decode.float)
(field "lon" Json.Decode.float)
Another complication you often have to deal with is nested sub-objects:
{
"type": "marker",
"location": {
"lat": 25.1212,
"lon": 55.1535
},
"attrs": {
"name": "Rixos The Palm Dubai",
"description": "Hotel"
}
}
To decode this JSON into a record with a matching structure, you need to write two decoders to decode the fields in location
and attrs
and then combine them using one of the map*
functions:
type alias Marker =
{ type : String
, location : MarkerLocation
, attrs : MarkerAttrs
}
type alias MarkerLocation =
{ lat : Float
, lon : Float
}
type alias MarkerAttrs =
{ name : String
, description : String
}
decodeMarker : Json.Decode.Decoder Marker
decodeMarker =
Json.Decode.map3 Marker
(field "type" Json.Decode.string)
(field "location" decodeMarkerLocation)
(field "attrs" decodeMarkerAttrs)
decodeMarkerLocation : Json.Decode.Decoder MarkerLocation
decodeMarkerLocation =
Json.Decode.map2 MarkerLocation
(field "lat" Json.Decode.float)
(field "lon" Json.Decode.float)
decodeMarkerAttrs : Json.Decode.Decoder MarkerAttrs
decodeMarkerAttrs =
Json.Decode.map2 MarkerAttrs
(field "name" Json.Decode.string)
(field "description" Json.Decode.string)
This scales to multiple levels of nesting.
Note that JSON-to-Elm is able to generate such nested decoders for you.
I talk more about JSON decoding and other Elm topics in my book, Practical Elm. Scroll down to grab a sample chapter (by the way, the book is available for a lower intro price until 26 June 2018).