---
title: Getting Unstuck with Elm JSON Decoders
teaser: Solving a `Maybe` problem is often easier than solving a `Decode` one.
tags: elm,web
author: Joël Quenneville
published_on: 2019-08-08
---

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 [`Maybe`s] 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.

[JSON decoders]: https://package.elm-lang.org/packages/elm/json/latest/Json-Decode
[`Maybe`s]: https://package.elm-lang.org/packages/elm/core/latest/Maybe

![Diagram: JSON to Maybe back to JSON](https://images.thoughtbot.com/jq-maybe-json-decoders/r8jQDU9DRcSZ8Q24QDrA_json-to-maybe-to-json.png)

## Decoding a record

Say we have a `User` record that looks like this:

```elm
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 User` if all the component parts were
> `Maybe`s?

Start by making concrete functions for those component parts, the maybe name and
maybe age:

```elm
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:

```elm
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:

```elm
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 User` if all the component parts were
> `Decoder`s?

Sound familiar? We can translate our `Maybe` solution pretty much directly into
a `Decoder` solution:

```elm
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:

```elm
type User = Admin String | RegularUser String
```

and we're getting JSON that looks like:

```json
{ "role": "admin"
, "email": "admin@example.com"
}
```

How can we decode this? Let's translate the problem into `Maybe`. Given some
optional primitives like:

```elm
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`].

```elm
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:

```elm
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].

```elm
maybeUser : Maybe User
maybeUser =
  maybeRole
    |> Maybe.andThen chooseFromRole
```

All together, this looks like:

```elm
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:

1. Replace uses of the _type_ `Maybe` with `Decoder`
2. Replace uses of the _module_ `Maybe` with `Decode`
3. Re-implement the "primitive" functions in terms of the `Decode` module's
   primitives (`int`/`float`/`string`/`bool`) and possibly `field`.
4. Replace uses of `Nothing` with `Decode.fail`
5. Any remaining uses of `Just` can probably be `Decode.succeed`
6. Tweak the names of your functions 😎

The end result is uncannily similar to our `Maybe` solution:

```elm
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
```

[custom type]: https://guide.elm-lang.org/types/custom_types.html
[complex decoding problems]: https://thoughtbot.com/blog/5-common-json-decoders
[equivalent to a `Maybe.map`]: https://thoughtbot.com/blog/maybe-mechanics#helper-functions---unwrapping-and-re-wrapping
[equivalent to the built-in `Maybe.andThen` helper]: https://thoughtbot.com/blog/maybe-mechanics#helper-functions---nested-maybes

## 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.

[behave the same as the equivalent functions on `Json.Decode`]: https://thoughtbot.com/blog/elms-universal-pattern
[powerful debugging]: https://thoughtbot.com/blog/classical-reasoning-and-debugging#reasoning-by-analogy
[reasoning by analogy]: https://thoughtbot.com/blog/reasoning-by-analogy
