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 required
s 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" 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.
- 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 (
Dog
capitalized) that you automatically get for declaring a type alias.