Elm’s JSON decoders can be tricky to wrap your head around. You’re trying to decode a value and aren’t coming up with anything useful. How do you get unstuck?
One solution I use is to try to simulate the problem using Maybes instead of
decoders. Then once I have a solution to the Maybe problem, I can use it to
help guide me towards the equivalent decoder solution.

Decoding a record
Say we have a User record that looks like this:
type alias User = { name : String, age : Int }
We’re struggling to figure out how to decode this. Instead of banging our heads against the wall, let’s try to solve an easier problem:
How could we construct a
Maybe Userif all the component parts wereMaybes?
Start by making concrete functions for those component parts, the maybe name and maybe age:
maybeName : Maybe String
maybeName =
Just "John Doe"
maybeAge : Maybe Int
maybeAge =
Just 42
Great! Now how can you combine those to get a Maybe User? You might start
with some nested case statements. Eventually you realize those nested case
statements are equivalent to Maybe.map2. You end up with the following
solution:
maybeUser : Maybe User
maybeUser =
Maybe.map2 User maybeName maybeAge
OK, so we know how to assemble a Maybe User out of a Maybe name and age. How
does that help us decode JSON? Well we probably already know how to get some
primitives out of JSON such as the name and the age. Let’s start there:
nameDecoder : Decoder String
nameDecoder =
Decode.field "name" Decode.string
ageDecoder : Decoder Int
ageDecoder =
Decode.field "age" Decode.int
The real question then becomes:
How could we construct a
Decoder Userif all the component parts wereDecoders?
Sound familiar? We can translate our Maybe solution pretty much directly into
a Decoder solution:
userDecoder : Decoder User
userDecoder =
Decode.map2 User nameDecoder ageDecoder
More Complex example (custom types)
OK, that was a roundabout way to figure out how to decode a record. Every
decoding tutorial online shows how to decode these directly without doing any of
this Maybe nonsense. How do we deal with more complex decoding problems?
This technique still holds up.
We have a custom type that represents the users of our app:
type User = Admin String | RegularUser String
and we’re getting JSON that looks like:
{ "role": "admin"
, "email": "admin@example.com"
}
How can we decode this? Let’s translate the problem into Maybe. Given some
optional primitives like:
maybeRole : Maybe String
maybeRole =
Just "admin"
maybeEmail : Maybe String
maybeEmail =
Just "admin@example.com"
How can we combine them to get a
Maybe User?
As a first step we need to construct either an admin or a regular user from the
email. We could use case statements to unwrap the email but it turns out that’s
equivalent to a Maybe.map.
maybeAdmin : Maybe User
maybeAdmin =
Maybe.map Admin maybeEmail
maybeRegularUser : Maybe User
maybeRegularUser =
Maybe.map RegularUser maybeEmail
We don’t want both of these. Instead we want to choose one based on a role. Let’s write a choosing function:
chooseFromRole : String -> Maybe User
chooseFromRole role =
case role of
"admin" -> maybeAdmin
"regular" -> maybeRegularUser
_ -> Nothing
Finally, we want to unwrap the Maybe role and call our choosing function based
on it. Again we can write some case statements but they would be equivalent to
the built-in Maybe.andThen helper.
maybeUser : Maybe User
maybeUser =
maybeRole
|> Maybe.andThen chooseFromRole
All together, this looks like:
maybeUser : Maybe User
maybeUser =
maybeRole
|> Maybe.andThen chooseFromRole
-- CHOOSE AN IMPLEMENTATION
chooseFromRole : String -> Maybe User
chooseFromRole role =
case role of
"admin" -> maybeAdmin
"regular" -> maybeRegularUser
_ -> Nothing
-- BUILD ADMIN/REGULAR
maybeAdmin : Maybe User
maybeAdmin =
Maybe.map Admin maybeEmail
maybeRegularUser : Maybe User
maybeRegularUser =
Maybe.map RegularUser maybeEmail
-- PRIMITIVES
-- THIS IS WHAT WE STARTED WITH
maybeRole : Maybe String
maybeRole =
Just "admin"
maybeEmail : Maybe String
maybeEmail =
"admin@example.com"
Use the following steps to convert this solution to a Json.Decode solution:
- Replace uses of the type
MaybewithDecoder - Replace uses of the module
MaybewithDecode - Re-implement the “primitive” functions in terms of the
Decodemodule’s primitives (int/float/string/bool) and possiblyfield. - Replace uses of
NothingwithDecode.fail - Any remaining uses of
Justcan probably beDecode.succeed - Tweak the names of your functions 😎
The end result is uncannily similar to our Maybe solution:
userDecoder : Decoder User
userDecoder =
roleDecoder
|> Decode.andThen chooseFromRole
-- CHOOSE AN IMPLEMENTATION
chooseFromRole : String -> Decoder User
chooseFromRole role =
case role of
"admin" -> adminDecoder
"regular" -> regularUserDecoder
_ -> Decode.fail ("Invalid user type: " ++ string)
-- BUILD ADMIN/REGULAR
adminDecoder : Decoder User
adminDecoder =
Decode.map Admin emailDecoder
regularUserDecoder : Decoder User
regularUserDecoder =
Decode.map RegularUser emailDecoder
-- PRIMITIVES
roleDecoder : Decoder String
roleDecoder =
Decode.field "role" Decode.string
emailDecoder : Decoder String
emailDecoder =
Decode.field "email" Decode.string
Why does this work?
The Maybe type is easier to grasp. It feels more concrete than Decoder. It
also exposes it’s constructors allowing us pattern-match and case on Just and
Nothing. We can take Maybe apart and put it back together, building an
intuition for what it represents and how it works. Most Elm devs will find it
easier to solve a problem in terms of Maybe rather than Json.Decode.
Maybe has another important property. Most of its key helper functions like
map and andThen behave the same as the equivalent functions on Json.Decode.
This means we can take lessons learned while solving the easier Maybe problem
and transfer them to solving the harder decoder problem.
Translating hard problems into an easier problem space, solving the easy problem, and then translating the easy solution back to help solve the initial hard problem is a form of reasoning by analogy. This is a powerful debugging and general learning technique that’s well worth including in your toolkit.