Explore our categories

MEGA – Enchanting decoders (part 1)

Make Elm Great Again — Ep. 3

In this MEGA’s article, we’ll see some useful patterns to discover magic powers hidden inside Elm decoders.

Introduction: The gate to darkness

Once upon a time, in the Pure Language realm, everything was simple, bright and predictable.

That was until a despicable programmer fairy opened the gates to the realm of Impure Languages…

This could quite easily be your story as an Elm beginner. You were used to writing clean and composable code. But then you realised that everything’s sandboxed. So you still need to deal with stuff like browser, JavaScript and API calls.

And that’s when you probably met your first decoder.

A programmable parser

A decoder is a piece of code that builds a parser to turn your serialized data into your Elm types. Otherwise, it fails.

It’s needed every time you need to obtain data at runtime. For instance, events, API calls, ports and subscriptions.

Out of the box Elm ships with basic JSON parsing capabilities, that allow you to deal with JavaScript and REST API. Some libraries empower JSON decoding. And others allows us to deal with data different to JSON (like GraphQL).

In the following examples, I’ll show some interesting (I hope) cases of JSON parsing of a rest API. And I’ll use elm-json-decode-pipeline, a great package that we use a lot here at Prima.

Master decoders and keep your world fair

Granted, decoders can be a bit of a headache. But once you master them, you can rid your Elm code of a lot of data consistency checks.

Decoders allow you to move all that “if-then” logic in one single point. You can then build your application model not around the backend, but around the user application needs and keep your code cleaner.

Do you remember the examples from Maybe maybe is not the right choice article?

Let’s imagine the situation where Gift is provided by a REST API like the following (I’m using jsdoc to describe explicit nullable fields, so imagine a case where we have some kind of type guarantee from BE). And you’re writing a Payment application:

 * @typedef {{
 * hasGreetingCard: !boolean,
 * gCTitle: string|null,
 * gCBody: string|null,
 * gCSignature: string|null,
 * price: number|null
 * }} Gift
 */Code language: JSON / JSON with Comments (json)

Let’s reorder messy types

Let’s consider the price key first.

Do we need to have Maybe Float in our model?

The answer’s no, we don’t need that Maybe at all!

The answer’s no. Because the API contract says that, in every legal case that value exists, there’s a number type, and it’s always a price. So a more suitable data type for the frontend could be a Decimal of decimal package.

The package exposes a nice function:

fromFloat : Float -> Maybe DecimalCode language: Elm (elm)

So now we have two options:

  1. The not-so smart option. Decode a Maybe Float and keep that in our model. Every time we’ll need it we’ll turn that into a Decimal resolving two Maybes. Since we can’t handle Nothing variant (it’s a payment page) we’ll probably resolve the Nothing with a default.
  2. The smart option. Attempt to decode our Maybe Float into a Decimal and if we can’t… well probably something is going very wrong outside there!

Since we want to be smart let’s write our decimal decoder joining together existing ones with a pinch of custom logic to make it decode a Decimal or fail:

decimalFromFloatDecoder : JsonDecode.Decoder Decimal
decimalFromFloatDecoder =
     |> JsonDecode.andThen
         (\decodedFloat ->
             -- Float -> Maybe Decimal
             Decimal.fromFloat decodedFloat
                 -- if Just decimal -> success
                 |> Maybe.map JsonDecode.succeed
                 -- if Nothing -> failure
                 |> Maybe.withDefault (decodingFailure decodedFloat)

decodingFailure : Float -> JsonDecode.Decoder Decimal
decodingFailure decodedFloat =
    "Can't turn "
        ++ String.fromFloat decodedFloat
        ++ " into a decimal"
        |> JsonDecode.failCode language: Elm (elm)

Now it’s time for Json.Decode.Pipeline ! We can use its required function to ensure that the price field exists and our new decoder to turn it into a Decimal :

type alias Gift =
    { price : Decimal }

giftDecoder : JsonDecode.Decoder Gift
giftDecoder =
    JsonDecode.succeed Gift
        |> JDPipeline.required "price" decimalFromFloatDecoderCode language: Elm (elm)

That’s all. Now your Gift model has its nice and well typed price field!

Decode union types

So now imagine that we want to decode the Gift type with its optional greeting card pieces of information.

The first naive approach is to write a very simple decoder (because you know they can be a headache) that decodes a record like this:

type alias Gift =
    { hasGreetingCard : Bool
    , greetingCardTitle : Maybe String
    , greetingCardBody : Maybe String
    , greetingCardSignature : Maybe String
    , price : Decimal
   }Code language: JavaScript (javascript)

Instead of something better shaped like:

type Gift
    = Simple { price : Decimal }
    | WithGreetingCard { price : Decimal, greetingCard : GreetingCard}
type alias GreetingCard =
    { title : String
    , body : String
    , signature : String
    }Code language: JavaScript (javascript)

As seen in the previous article, we prefer the second choice. So we’d need to convert that record into our union type.

But how?

Well, with a well-structured decoder, we can make all the “bogus” cases collapse into the decoding failure and clean everything up.

When you work with decoders, it helps to think locally. So we’ll try to break down the big problem into simpler tasks.

First of all, we’ll try to decode the raw pieces of information that we need to build a SimpleGift:

type alias SimpleRaw =
    { price : Decimal, hasGreetingCard : Bool }

simpleRawDecoder : Decode.Decoder SimpleRaw
simpleRawDecoder =
    Decode.succeed SimpleRaw
        |> DecodePipeline.required "price" decimalFromFloatDecoder
        |> DecodePipeline.required "hasGreetingCard" Decode.boolCode language: JavaScript (javascript)

Let’s do the same with the other variant, reusing the previous decoders. We could rewrite a whole decoder. But I’m lazy and I can use Json.Decode.Pipeline.custom function:

type alias WithGreetingCardRaw =
    { simpleRaw : SimpleRaw
    , body : String
    , signature : String
    , title : String

withGreetingCardRawDecoder : Decode.Decoder WithGreetingCardRaw
withGreetingCardRawDecoder =
    Decode.succeed WithGreetingCardRaw
        |> DecodePipeline.custom simpleRawDecoder
        |> DecodePipeline.required "gCBody" Decode.string
        |> DecodePipeline.required "gCSignature" Decode.string
        |> DecodePipeline.required "gCTitle" Decode.string
rawToWithGreetingCardDecoder : WithGreetingCardRaw -> Decode.Decoder Gift
rawToWithGreetingCardDecoder rawData =
    if rawData.simpleRaw.hasGreetingCard then
            { price = rawData.simpleRawData.price
            , greetingCard =
                { title = rawData.title
                , body = rawData.body
                , signature = rawData.signature
            |> Decode.succeed
        Decode.fail "Not has greeting card. It can't be WithGreetingCard variant"

giftWithGreetingCardDecoder : Decode.Decoder Gift
giftWithGreetingCardDecoder =
        |> Decode.andThen rawToWithGreetingCardDecoderCode language: JavaScript (javascript)

Now we can simply combine them with Decode.oneOf. This function tries to resolve the serialized data with a list of decoders until one succeeds (so pay attention to priority, in case you don’t have a discriminating value like hasGreetingCard):

module MyApp.Gift exposing (Gift, fetch, decoder)
decoder : Decode.Decoder Gift
decoder =
        [ simpleDecoder
        , withGreetingCardDecoder
fetch: (Result Http.Error Gift -> msg) -> Cmd msg
fetch msg = ...Code language: JavaScript (javascript)

That could become (with some love and modularization that I’ll leave to you) something like:

module Gift exposing (Gift, decoder)

import Gift.SimpleGift as SimpleGift exposing (SimpleGift)
import Gift.WithGreetingCardGift as WithGreetingCardGift exposing (WithGreetingCardGift)

type Gift
    = Simple SimpleGift
    | WithGreetingCard WithGreetingCardGift
decoder : Decode.Decoder Gift
decoder =
        [ SimpleGift.decoder 
           |> Decode.map Simple
        , WithGreetingCardGift.decoder |> Decode.map WithGreetingCard
        ]Code language: JavaScript (javascript)

And that’s it. We’ve moved all our data coherence checks into a single point. Plus, we can expose only a simple fetch function, the whole decoder, or both as preferred.

We’ve reached more than one goal with that:

  • We wiped out a loosely typed maybish record in favour of a more fashionable union in the whole app.
  • We can move the whole business logic and consistency checks about Gift in a single point.
  • We can make our consistency checks more or less strict based on our BE and requirements. We can morph the raw data directly here, providing default cases instead of making everything fail. For example what happens if gift card values are truly nullable but you always provide them even if not given (with fallback values)? You can simply provide the decoder with your fallback in case of null:
greetingCardRawDataDecoder : Decode.Decoder GreetingCardRawData
greetingCardRawDataDecoder =
    Decode.succeed GreetingCardRawData
        |> DecodePipeline.custom simpleGiftRawDataDecoder
        |> DecodePipeline.required "gCBody"
            (stringOrDefaultDecoder "Your greeting message")
        -- ...

stringOrDefaultDecoder : String -> Decode.Decoder String
stringOrDefaultDecoder def =
    Decode.nullable Decode.string
        |> Decode.map (Maybe.withDefault def)Code language: JavaScript (javascript)

Intercept errors and keep your flows clean

Surely you’ve noticed how messy your update function becomes when you have more than one API call and a little logic (like some security checks on it):

type AppMsg
    = GetGiftResult (Result Http.Error Gift)
    | GetAnotherResult (Result Http.Error Another)
appUpdate : AppMsg -> Model -> Update
appUpdate msg model =
    case msg of
        GetGiftResult (Result.Ok gift) ->
            -- store and then do something

        GetGiftResult (Result.Err httpError) ->
            case httpError of
                Http.BadStatus 401 ->
                    -- Unauthorized handling code

                _ ->
                    -- show generic error

        GetAnotherResult (Result.Ok another) ->
            -- store and then do something

        GetAnotherResult (Result.Err httpError) ->
            case httpError of
                Http.BadStatus 401 ->
                    -- Unauthorized handling code

                _ ->
                    -- show generic error
        --...Code language: JavaScript (javascript)

You have to admit that it’s not nice or scalable. A lot of branches are probably the same. You can group the logic with functions but the update case will stay huge.

We can do better. Let me explain.

You probably noticed that your http calls always produce a Result Http.Error decodedData as come back result. But Elm’s faces 2 possible failures.

The first is during the request process (that always comes out as a Response type):

-- Http.elm

type Response body
  = BadUrl_ String
  | Timeout_
  | NetworkError_
  | BadStatus_ Metadata body
  | GoodStatus_ Metadata bodyCode language: JavaScript (javascript)

The other possible failure happens during the decoding process and results in a parsing error:

-- Decoder.elm

type Error
  = Field String Error
  | Index Int Error
  | OneOf (List Error)
  | Failure String ValueCode language: JavaScript (javascript)

This because you’re used to passing Http.expectJson as request expectation:

fetch :
    (Result Http.Error Gift -> msg)
    -> AuthToken
    -> Cmd msg
fetch tag authToken =
        { method = "GET"
        , headers = [ AuthToken.header authToken ]
        , url = "https://my.api.it/get-my-gift"
        , body = Http.emptyBody
        , expect = Http.expectJson tag decoder
        , timeout = Nothing
        , tracker = Nothing
        }Code language: JavaScript (javascript)

Such function takes the internal Response body type, if the response is ok, then attempts to decode the body with your decoder.

If Response indicates a failure or decoder fails then the result will be a Http.Error type, that wraps the previous two errors:

type Error
  = BadUrl String -- Response.BadUrl_ case
  | Timeout -- Response.Timeout_ case
  | NetworkError -- Response.NetworkError_
  | BadStatus Int -- Response.BadStatus with metadata statusCode
  | BadBody String -- Decode.Error serialized as StringCode language: JavaScript (javascript)

It’s fast and simple. But there are a couple of drawbacks:

  1. It hides some Response information. (We can’t access the body in the case of a BadStatus. We can’t see response headers. And we can’t intercept decoding failures.)
  2. In the case of a reusable library, you’re demanding a lot of business logic of the consumer regarding the API.
  3. The component’s API can’t be self-explanatory.

We can fix almost all of these weak points though.

First of all, let’s define the Gift error that we’re interested to catch:

import MyApp.Unauthorized as Unauthorized exposing (Unauthorized)

type Error
    = UnauthorizedError Unauthorized
    | NotInterestingError StringCode language: JavaScript (javascript)

And after that, we can write our custom expect, that intercepts 401 status code and applies another decoder on that body:

expect : (Result Error Gift -> msg) -> Http.Expect msg
expect tag =
    Http.expectStringResponse tag responseResolver

responseResolver : Http.Response String -> Result Error Gift
responseResolver response =
    case response of
        Http.BadUrl_ string ->
            "Bad Url: "
                ++ string
                |> NotInterestingError
                |> Result.Err

        Http.Timeout_ ->
            NotInterestingError "Timeout"
                |> Result.Err

        Http.NetworkError_ ->
            NotInterestingError "NetworkError"
                |> Result.Err

        Http.BadStatus_ metadata b ->
            if metadata.statusCode == 401 then
                case JDecode.decodeString Unauthorized.decoder b of
                    Result.Ok data ->
                        UnauthorizedError data
                            |> Result.Err

                    Result.Err decodeError ->
                        "Decode unauthorized response failure: "
                            ++ JDecode.errorToString decodeError
                            |> NotInterestingError
                            |> Result.Err

                    ++ String.fromInt metadata.statusCode
                    |> NotInterestingError
                    |> Result.Err

        Http.GoodStatus_ _ body ->
            JDecode.decodeString decoder body
                |> Result.mapError
                    (\err ->
                        "Decode gift failure: "
                            ++ JDecode.errorToString err
                            |> NotInterestingError
                    )Code language: JavaScript (javascript)

We can now make our fetch function more expressive:

type alias FetchArgs =
    { authToken : AuthToken
    , onGiftFetched : Gift -> msg
    , onUnauthorized : UnauthorizedData -> msg
    , onError : String -> msg

fetch : FetchArgs -> Cmd msg
fetch args =
        { method = "GET"
        , headers = [ AuthToken.header args.authToken ]
        , url = "https://my.api.it/get-my-gift"
        , body = Http.emptyBody
        , expect = expect args
        , timeout = Nothing
        , tracker = Nothing
        }Code language: JavaScript (javascript)

And change our custom expect function, adding the message retagging:

expect : FetchArgs -> Http.Expect msg
expect args =
    Http.expectStringResponse (resultToTag args) responseResolver

resultToTag : FetchArgs -> Result Error Gift -> msg
resultToTag args result =
    case result of
        Ok gift ->
            args.onGiftFetched gift

        Err (Unauthorized unauthorizedData) ->
            args.onUnauthorized unauthorizedData

        Err (NotInterestingError errAsString) ->
            args.onError errAsStringCode language: JavaScript (javascript)

And finally, in our update function, we can collapse every common logic in one dedicated state, like this:

type AppMsg
    = GiftFetched Gift
    | AnotherThingFetched AnotherThing
    | Unauthorized UnauthorizedData
    | FatalError String
    -- ...
appUpdate : AppMsg -> Model -> Update
appUpdate msg model =
    case msg of
        GiftFetched gift ->
            -- store and then do something ...
        AnotherThingFetched anotherThing ->
            -- store and then do something ...
        Unauthorized unauthorized ->
            model -- here we can start a re-auth flow
               |> withCmd Unauthorized.fetchAuth unauthorized       

        FatalError errString ->
            model -- here we simply show a error page and log all
               |> Model.setFatalError errString 
               |> withCmd Logger.logError errString
         --...Code language: JavaScript (javascript)

Congratulations, you have reached the end…

If you’ve reached this point (without a headache), you’ve gained an entry-level Decoder Master.

Wait a minute… all of this and I’m still an entry-level??

Ehm yes… You’ve learned that decoders can be more than simple string parsers, and how to make them more effective in cleaning up a malformed server API.

You should be able to decode complex elm types and rewrite the basic “decode or fail” logic hidden inside HTTP requests.

This is great and helps make you a better Elm’s sorcerer. But there is still much to say about decoders. I’ll try to cover up the most interesting parts in future articles.

See you soon in the next castle!