Skip to content

Instantly share code, notes, and snippets.

@briandk
Created November 25, 2022 20:21
Show Gist options
  • Save briandk/633a49e102a8cc6df968626700dd1bb1 to your computer and use it in GitHub Desktop.
Save briandk/633a49e102a8cc6df968626700dd1bb1 to your computer and use it in GitHub Desktop.
Trying to understand why the order of `required` statements matters in an Elm JSON Decode Pipeline

Why does the order of required statements matter in a JSON Decode Pipeline?

Background

I’m currently working through the book Programming Elm by Jeremy Fairbank. I’m on Chapter 4, where he introduces JSON decoders and the Json.Decode.Pipeline package. Please bear with me, as I’m an Elm beginner. I’m also working in the Elm repl, as I’m not currently sure how to translate my work into a full Elm program.

What I Expect to Happen

In Elm, the order of requireds in a JSON decode pipeline shouldn’t matter for successful decoding, because we’re matching the names of the keys in the JSON oject to the names of the fields in a record. There is no inherent order to the fields of an Elm record, just as there is no inherent order to the properties of a JSON object.

What Actually Happens

If the order of required statements in a JSON decode pipeline doesn’t match the order of arguments to the function creating the record, you can end up with a decoder whose type expectations are wrong. This, I imagine, would cause all sorts of errors when actually decoding a JSON string.

Why I Think This Is a Problem

This feels brittle to me, given how much Elm tries to protect me from making silly mistakes. If I get the order of fields even slightly wrong in my decoder, I could get all sorts of type mismatches. I don't want that to happen. I want to write good, reliable code!

My Thinking Process/Steps to Reproduce

It seems like two records are equal if their field names and values are equal, even if the fields are in different orders

Consider the following example. I’ll create a custom type for a record (Dog), specifying the data types of each field, then create a function(dog) that takes a few arguments to create a record of that type.

import Json.Decode exposing (decodeString, float, int, string, succeed)
import Json.Decode.Pipeline exposing (required) -- available via `elm install NoRedInk/elm-json-decode-pipeline`


type alias Dog =
    { name : String
    , height : Float
    , age : Int
    }


dog : Float -> Int -> String -> Dog
dog height age name =
    { name = name
    , height = height
    , age = age
    }

Note that the argument order for the dog function is height, age, name, but the order in which I’ve declared them in the type alias Dog is name first, followed by height, followed by age. This order mismatch doesn’t seem to cause any problems with Elm, because as far as I can tell Elm will say two records are equal regardless of the order of the fields, provided the names and corresponding values of the fields are equal. For example:

{ a = 1, b = 2 } == { b = 2, a = 1 } -- Evaluates to True

dog 3.14 11 "Tucker" == { age = 11, name = "Tucker", height = 3.14 } -- Evaluates to True, even though the record literal I'm comparing it to has the fields in a different order than the `dog` function expects them.

So, the record fields in the type alias are in one order, the arguments to my dog function are in another order, and the record fields in my literal are in a third order, but all that seems OK.

It also seems like a Json.Decode.Pipeline needs the order of field names/types to match the order of arguments to the function that creates a record

Next, though, consider the following decoder:

dogDecoder =
    succeed dog
        |> required "age" int
        |> required "name" string
        |> required "height" float

The return type of dogDecoder is:

<internals>
    : Json.Decode.Decoder { age : String, height : Int, name : Float }

☝️This would seem to be a problem because according to my type alias and my dog function, age should be of type Int, but this decoder seems to expect age to be a String. In fact, none of the data this decoder expects to see has the proper type.

If we swap name to the first position in the decoding pipeline:

dogDecoder =
    succeed dog
        |> required "name" string
        |> required "age" int
        |> required "height" float

we get:

<internals>
    : Json.Decode.Decoder { age : Int, height : String, name : Float }

☝️This decoder is slightly better. It’s at least expecting age as an Int, which I think is happening because age is the second argument to my dog function and here my pipeline puts age second. But the types of the other two fields the decoder expects are incorrect.

What I'd Like Help With

  1. Am I doing something wrong with how I'm using the JSON Decode Pipeline?
  2. If I'm not doing anything wrong, is there a less brittle/more robust decoding solution I should be using that doesn't require me to get field order exactly right in order to work?

Please let me know if there's anything else I can clear up. Thanks!

@briandk
Copy link
Author

briandk commented Nov 25, 2022

The consensus seems to be: use an explicit function in your decoder (whether named or anonymous) to allow the compiler to help you.

That's what it turns outdog (lowercase) is: an explicit associator of argument order to field.

The other recommendation was to avoid using the constructor (Dog capitalized) that you automatically get for declaring a type alias.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment