---
title: 5 Common JSON Decoders
teaser: Elm JSON decoding in 5 common scenarios
tags: elm,web
author: Joël Quenneville
published_on: 2017-12-04
---

You've learned the basics of decoding JSON in Elm and are comfortable converting
[JSON into records]. However, real life has a tendency to give you tricky JSON
to work with that doesn't fit neatly with this approach.

Here are five scenarios I commonly run into and how to decode them.

In all these examples, you can assume the decode library has been imported like:

```elm
import Json.Decode as JD exposing (Decoder)
```

[JSON into records]: https://guide.elm-lang.org/interop/json.html#decoding-objects

## 1 - Decoding union types

Oftentimes you will want to express a limited set of values as a union type:

```elm
type Direction = North | South | East | West
```

JSON doesn't support values like this and will probably send these values down
as strings or integers. We'll need to decode this in two steps: first decode the
string, and then turn the string into a direction. Because we might get an
invalid string value, we need to handle errors too.

```elm
direction : Decoder Direction
direction =
  JD.string |> JD.andThen directionFromString

directionFromString : String -> Decoder Direction
directionFromString string =
  case string of
    "north" -> JD.succeed North
    "south" -> JD.succeed South
    "east" -> JD.succeed East
    "west" -> JD.succeed West
    _ -> JD.fail ("Invalid direction: " ++ string)
```

[See it in action in Ellie](https://ellie-app.com/7J56cjt4Wa1/0).

## 2 - Separate decoding and parsing union types

If you are parsing union types from strings elsewhere in your code, you may want
to separate the parsing from the decoding. Write a regular parsing function that
returns a `Result` and write a function that can convert that `Result` into a
`Decoder`.

```elm
parseDirection : String -> Result String Direction
parseDirection string =
  case string of
    "north" -> Ok North
    "south" -> Ok South
    "east" -> Ok East
    "west" -> Ok West
    _ -> Err ("Invalid direction: " ++ string)
```

This is almost exactly the same as our decoder from the previous section except
that it returns `Result` directly without having to go through the extra step of
running the decoder.

It would be nice not to duplicate the case statement and instead have some way
of implementing our decoder in terms of `parseDirection`. We can do that if we
have a way to turn `Result`s into `Decoder`s.

```elm
fromResult : Result String a -> Decoder a
fromResult result =
  case result of
    Ok a -> JD.succeed a
    Err errorMessage -> JD.fail errorMessage
```

This function is so useful that it also exists in the third-party package
json-extra as the [`fromResult`] function.

Finally we can put it all together with:

```elm
direction : Decoder Direction
direction =
  JD.string |> JD.andThen (fromResult << parseDirection)
```

[See it in action in Ellie](https://ellie-app.com/7LcWBJv9Za1/0)

[`fromResult`]: http://package.elm-lang.org/packages/elm-community/json-extra/2.6.0/Json-Decode-Extra#fromResult

## 3 - Additional parsing

Sometimes, you need to parse out a type that JSON doesn't support such as a
date. You can use the same parse and decode approach we used for the union
types:

```elm
date : Decoder Date
date =
  JD.string |> JD.andThen (fromResult << Date.fromString)
```

It is common for backends to mistakenly encode numbers as JSON strings rather
than numbers.  To read them as numbers in Elm, we'll need to use this parse and
decode approach:

```elm
stringInt : Decoder Int
stringInt =
  JD.string |> JD.andThen (fromResult << String.toInt)
```

[See both of these in action in Ellie](https://ellie-app.com/7QSzknRvTa1/0)

## 4 - Conditional decoding based on the shape of the JSON

Sometimes your JSON can be in multiple shapes and you'd like to decode it
differently based on the shape. Here we have a payload that may or may not have
an email depending on whether the user is signed in.

```json
{ "email": "user@example.com",
  "otherField": "foo"
}
```

versus

```json
{ "otherField": "foo"
}
```

We might model that on the Elm side with:

```elm
type User = Guest | SignedIn String
```

First create decoders for each individual kind of user:

```elm
guestDecoder : Decoder User
guestDecoder =
  JD.succeed Guest

signedInDecoder : Decoder User
signedInDecoder =
  JD.map SignedIn (JD.field "email" JD.string)
```

`Json.Decode.oneOf` allows you to specify a list of possible decoders. It will
go through the list in order and use the first one that can successfully decode
the JSON. Finally we can write a `userDecoder` that looks like:

```elm
userDecoder : Decoder User
userDecoder =
  JD.oneOf [signedInDecoder, guestDecoder]
```

There are some gotchas around this technique. Order is really important and your
different JSONs must have _different_ shapes. For example:

```elm
JD.oneOf [guestDecoder, signedInDecoder]
```

will _always_ decode a guest. This is because `guestDecoder` cannot fail so
`oneOf` will never attempt other decoders after it.

If your JSON always has the same shape but depending on some of the values you
want to decode differently, you'll need to use a different approach based on
`Json.Decode.andThen`.

[See it in action in Ellie](https://ellie-app.com/7WmZfXZvBa1/0)

## 5 - Conditional decoding based on a field

Say you have two kinds of users but the JSON payload have the same shape:

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

versus

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

We could model this in Elm as:

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

Again, we start by writing decoders for each case:

```elm
adminDecoder : Decoder User
adminDecoder =
  JD.map Admin (JD.field "email" JD.string)

regularUserDecoder : Decoder User
regularUserDecoder =
  JD.map RegularUser (JD.field "email" JD.string)
```

The JSON for both of these would have the same shape so we can't use `oneOf`.
Instead, we'll have to decode based on the values of some of the fields in the
JSON. In this particular case, the backend team have helpfully added a `role`
field to the JSON that will be either `"regular"` or `"admin"`:

```elm
userFromType : String -> Decoder User
userFromType string =
  case string of
    "regular" -> regularUserDecoder
    "admin" -> adminDecoder
    _ -> JD.fail ("Invalid user type: " ++ string)

userDecoder : Decoder User
userDecoder =
  JD.field "role" JD.string
    |> JD.andThen userFromType
```

[See it in action in Ellie](https://ellie-app.com/7ZbDMMvh8a1/0).

## General tips

When you need a different decoder depending on context, start with the smallest
pieces by writing a small decoder for each case.

Once you have decoders for each case, look to see _how_ you will know which
decoder to pick. Write a function that picks the right decoder based on your
decision.

Finally, put it all together using functions like `JD.andThen`.
