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.
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.
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.
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!
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" floatThe 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" floatwe 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.
- Am I doing something wrong with how I'm using the JSON Decode Pipeline?
- 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!
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 out
dog(lowercase) is: an explicit associator of argument order to field.The other recommendation was to avoid using the constructor (
Dogcapitalized) that you automatically get for declaring a type alias.