Elm JSON Decoders

Most bugs in frontend code don’t come from the stuff you see, but from the stuff you expect. Especially when you’re working with data from APIs. A value that’s supposed to be a string turns out to be null. Maybe someone renamed a field on the backend. Now your code throws errors, or worse, things look broken for users.

Elm makes this pain go away. With decoders, you can turn messy JSON into safe Elm types. The best part? If something doesn’t match, Elm doesn’t crash. It won’t let you use data that isn’t shaped right. No more runtime surprises.

Let’s walk through a real example.

Here’s the code. It fetches users from an API and lets you filter by all, odd, or even IDs. The magic is how Elm turns raw JSON into a list of User records, with no chance of “undefined is not a function”.

The User Type

First, we say exactly what a user should look like:

type alias User =
    { id : Int
    , avatar : String
    , firstName : String
    , lastName : String
    }

No maybes, no nulls. Either the data is shaped like this, or Elm will never let it in.

Decoding JSON Safely

This is where decoders come in. A decoder checks the shape of the data, field by field:

decodeUser : Decode.Decoder User
decodeUser =
    Decode.map4 User
        (Decode.field "id" Decode.int)
        (Decode.field "avatar" Decode.string)
        (Decode.field "first_name" Decode.string)
        (Decode.field "last_name" Decode.string)
  • Decode.field "id" Decode.int means “find the id field, make sure it’s an integer”.
  • Decode.field "avatar" Decode.string does the same for the avatar, and so on.

The whole user is only built if all four fields are present and correct.

When fetching multiple users, we decode a list:

decodeUsers : Decoder (List User)
decodeUsers =
    Decode.at [ "data" ] (Decode.list decodeUser)

This looks inside the data field of the API response, and decodes each item as a User.

Zero Runtime Errors

If the API changes or sends bad data, Elm will never run with broken user objects. Instead, it will give you a clear error message, like “expected a String but got null”. You can show a nice error to the user, or try again, no weird crashes.

Fetching the Data

Elm puts it together like this:

getUser : Cmd Msg
getUser =
    Http.get (apiUrl "/users") decodeUsers
        |> RemoteData.sendRequest
        |> Cmd.map UsersResponse
  • Fetch the users from the API.
  • Try to decode them into a list of users.
  • If it works, update the model. If it doesn’t, you can handle the error.

What Makes This Powerful

  • Strong types: You always know what you’re working with.
  • No silent failures: Broken data is caught immediately.
  • Simple error handling: Bad data means a clear message, not a blank page.

A Quick Note on Filtering

The app even filters users by odd or even ID, just for fun:

filter : FilterType -> List User -> List User
filter filterType users =
    case filterType of
        All -> users
        Odd -> List.filter (\user -> user.id % 2 /= 0) users
        Even -> List.filter (\user -> user.id % 2 == 0) users

This all works because we can trust our data is always shaped right.

With Elm’s decoders, you spend less time worrying about bad data and more time building features. The compiler and the decoder do the heavy lifting. There are no “undefined is not a function” bugs hiding in the dark. Everything is safe, clear, and predictable.

If you want frontends that just work, use Elm’s decoders and let the types guide you.


Published on .