Elm’s way of parsing JSON is completely different to what you’ll often see in non-functional languages. To make the types of your program fit, the JSON input has to be more or less regular. Optional fields are not great to work with.
For a project I work on, we have some JSON that defines a form. This form has to be rendered by our Elm frontend.
serverResponse =
"{ \\"url\\": { \\"tag\\": \\"input\\", \\"type\\": \\"text\\"
, \\"default\\": \\"test\\"
}
}"
This is a key-value map of arbitrary length, where the name field (“url” in the example) can have arbitrary values.
This has to be converted to a List of FormField:
type alias Input =
{ name : String, class : Maybe String
, default : String, type' : String
}
type alias Decimal =
{ name : String, class : Maybe String
, default : Float, type' : String
}
type alias SpecialCase = String
type FormField
= InputField Input
| DecimalField Decimal
| SpecialField SpecialCase
The special case is based on name field. For certain values, only the name has to be decoded.
Now let’s say that there is a special case, where the name of a key-value pair determines the return type (messy json, remember?) Things get tricky.
Because the key-value map is of arbitrary length, the Decode.object*
functions won’t be of much use. The only way to decode this json is to use Decode.keyValuePairs
:
keyValuePairs : Decoder a -> Decoder (List (String, a))
The problem is that the type of the second argument - the value decoder - depends on the result of the first argument - the key decoder. There is no way to get that result. Painted into a corner…
value to the rescue
For nasty problems like this, Decode
provides Decode.value
. This type makes it possible to put off decoding until later.
serverResponse =
"{ \\"url\\": { \\"tag\\": \\"input\\", \\"type\\": \\"text\\"
, \\"default\\": \\"test\\"
}
}"
partialDecodeFormField : Decoder (List (String, Decode.value))
partialDecodeFormField =
Decode.keyValuePairs Decode.value
Now, this (String, Decode.value)
list needs to be transformed to List FormField
.
-- andMap for Decoder
(|:) : Decoder (a -> b) -> Decoder a -> Decoder b
(|:) = Decode.object2 (<|)
-- withDefault for Decoder
withDefault : a -> Decoder a -> Decoder a
withDefault default decoder =
oneOf
[ decoder
, Decode.succeed default
]
decodeFormField : String -> Decoder FormField
decodeFormField name =
if name == "special" then
Decode.succeed (SpecialField name)
else
("type" := Decode.string) `Decode.andThen` \\type' ->
let constructDecimal class default =
DecimalField <| Decimal name class default type'
constructInput class default =
InputField <| Input name class default type'
in
case type' of
"decimal" ->
Decode.succeed constructDecimal
|: (withDefault Nothing ("class" := maybe string))
|: (withDefault 0 ("default" := float))
_ ->
Decode.succeed constructInput
|: (withDefault Nothing ("class" := maybe string))
|: (withDefault "" ("default" := string))
The working of the (|:)
is discussed here. Note that the combination with Decode.succeed
has the same effect as the Decode.object*
functions.
Now, to join everything together, we need to puzzle with the types. We have a list of names and a list of values, and want a List FormField
. First of all, let’s find a way to combine a name and a value.
feedName : (a -> Decoder b)
-> a -> Decode.Value
-> Result String b
feedName decoder name value =
Decode.decodeValue (decoder name) value
-- takes the name from the key and uses it in object construction
feedNameL : (a -> Decoder b)
-> List (a, Decode.Value)
-> List (Result String b)
feedNameL decoder elements =
List.map (\\(name, value) -> feedName decoder name value) elements
I’ve implented both the singluar and list version. Taking all these pieces and puzzling them together, we get:
decodeList : Decoder (List (Result String FormField))
decodeList =
Decode.map (feedNameL decodeFormField) partialDecodeFormField
Now there is a final level of Result wrapping. The Err e
cases can be ignored or accumulated for error reporting. That gives a final List FormField
.
conclusion
JSON in Elm is a little tricky, and feels very constrained at first. After a while, I’ve come to appreciate how Elm makes me think about the JSON schema that I use in various projects. For instance, when a field is missing, Elm will give an error. This may point to bugs in the backend, or bad design: an explicit empty value is often better than no value at all. No value can mean many things, and explicit empty value conveys information.
For sure, the decoding requires some practice, and familiarity with FP is very helpful (in particular, Applicative Functors can be of great use, more on that later.) But in essence, this is an information/education problem rather than a technical one.
Apendix: full code
import Html exposing (text)
import Json.Decode as Decode exposing
(Decoder, (:=), oneOf, maybe, string, float)
type FormField
= InputField Input
| DecimalField Decimal
| SpecialField String
type alias Input =
{ name : String, class : Maybe String
, default : String, type' : String
}
type alias Decimal =
{ name : String, class : Maybe String
, default : Float, type' : String
}
-- JSON HELPERS
withDefault : a -> Decoder a -> Decoder a
withDefault default decoder =
oneOf
[ decoder
, Decode.succeed default
]
(|:) : Decoder (a -> b) -> Decoder a -> Decoder b
(|:) = Decode.object2 (<|)
serverResponse =
"{ \\"url\\": { \\"tag\\": \\"input\\", \\"type\\": \\"text\\"
, \\"default\\": \\"test\\"
}
}"
partialDecodeFormField : Decoder (List (String, Decode.Value))
partialDecodeFormField =
Decode.keyValuePairs Decode.value
feedName : (a -> Decoder b)
-> a -> Decode.Value
-> Result String b
feedName decoder name value =
Decode.decodeValue (decoder name) value
-- takes the name from the key and uses it in object construction
feedNameL : (a -> Decoder b)
-> List (a, Decode.Value)
-> List (Result String b)
feedNameL decoder elements =
List.map (\\(name, value) -> feedName decoder name value) elements
decodeList : Decoder (List (Result String FormField))
decodeList =
Decode.map (feedNameL decodeFormField) partialDecodeFormField
decodeFormField : String -> Decoder FormField
decodeFormField name =
if name == "special" then
Decode.succeed (SpecialField name)
else
("type" := Decode.string) `Decode.andThen` \\type' ->
let constructDecimal class default =
DecimalField <| Decimal name class default type'
constructInput class default =
InputField <| Input name class default type'
in
case type' of
"decimal" ->
Decode.succeed constructDecimal
|: (withDefault Nothing ("class" := maybe string))
|: (withDefault 0 ("default" := float))
_ ->
Decode.succeed constructInput
|: (withDefault Nothing ("class" := maybe string))
|: (withDefault "" ("default" := string))
main =
Decode.decodeString decodeList serverResponse
|> Result.withDefault []
|> toString
|> text