Composing Decoders like LEGO

First-time Elm users have a hard time learning how to compose decoders. People tend to get stuck on what they all mean, but it’s not too hard: just think of them like LEGO.

LEGO

LEGO bricks operate on a simple principle: there are holes in the bottoms that match the studs on top. Click the two together and you can build things. This interface is important! It’s been standard since 1958. It also means that you can connect LEGO with Duplos. This standard interface means that you can snap together more or less every Lego brick ever sold. Pretty impressive!

LEGO workers load some JSON decoders onto a plane. LEGO World 2013 by Brickset

Elm’s decoders are like that. It sounds like a ridiculous claim to make, but they are. A decoder exposes a standard interface that can snap together in any way you can think up. The docs say it best:

Represents a way of decoding JSON values. If you have a (Decoder (List String)) it will attempt to take some JSON value and turn it into a list of strings. These decoders are easy to put together so you can create more and more complex decoders.

Let’s examine that claim.

Decoding a List of Integers

Say we have a single integer. Not the most complex data type in the world, but we still need to parse it. Elm provides us a int decoder to begin with, so we can do the following:

Decode.decodeString Decode.int "1" == Ok 1

Decode.decodeString just takes a decoder and a string to get back a Result . In this case, it’s a string. This is trivial, as it should be. But, like LEGO we can snap them together:

Decode.decodeString (Decode.list Decode.int) "[1]" == Ok [1]

Decode.list takes another Decode.Decoder and returns something that will decode a list of those values. In other words, we can snap any decoder we can think of into a list. The same thing happens for Decode.dict , Decode.object1..9 , and every other decoder.

Click… click… click… they snap together!

Decoding a List of Users

Let’s move on to a more complex example. How about a user with an email and ID?

type alias User = { id : Int , email : String }

Let’s set up a JSON decoder using object2 .

user : Decoder User user = Decode.object2 User ("id" := Decode.int) ("email" := Decode.string)

Now we’re using a bunch of decoders!

First object2 takes a function that takes two arguments and applies the given decoders in order. These decoders can, of course, be for any value. Snap… click… (For more on how this works: Decoding Large JSON objects: A Summary)

Next, the (:=) decoder takes a path and a decoder, and applies that decoder to the field. So above, we’re saying “take the int decoder, and apply it to the value found at the "id" path.” But, as usual, we can apply any decoder here. More clicking, getting closer…

So now we can take this user decoder and apply it to some string:

Decode.decodeString user "{\"id\":1,\"email\":\"test@example.com\"}"

We end up with Ok { id = 1, email = "test@example.com" } . Our LEGO castle is complete! But what if we need a list of users? We can snap the user decoder into a Decode.list and we’re done!

Decode.decodeString (Decode.list user) "[{\"id\":1,\"email\":\"test@example.com\"}]"

By the way, a pro tip: you should’t use object1..9 in your real code. Use elm-decode-pipeline instead. It lets you get rid of (:=) , which is one of the more confusing decoders.

Composed!

So now you know! Composing JSON Decoders in Elm is like building with LEGO: snap together bricks to make something bigger. This style of function composition shows up in more places in the Elm core, but JSON Decoding shows it off well.