Hey folks!
I just started a discussion with a friend at work, and wanted to learn more about why Elm has JSON decoders.
Here’s some context to help understand what I’m curious about.
Some context:
Using Flags
Here’s a simple demo of using Elm flags, and an example on Ellie (thanks, Luke!):
<html>
<head>
<!-- ... head stuff ... -->
</head>
<body>
<div id="app"></div>
<script src="/compiled-elm-app.js"></script>
<script>
Elm.Main.init({
node: document.getElementById('app'),
flags: {
person: { name: 'Ryan', age: 25 }
}
})
</script>
</body>
</html>
module Main exposing (main)
type alias Flags =
{ person : Person
}
type alias Person =
{ name : String
, age : Int
}
type alias Model =
Person
type Msg
= NoOp
main =
Browser.element
{ init = init
, update = \_ model -> ( model, Cmd.none )
, view = \person ->
Html.text ("Hello " ++ person.name ++ "!")
, subscriptions = always Sub.none
}
init : Flags -> (Model, Cmd Msg)
init flags =
( flags.person
, Cmd.none
)
Here is a simple Elm element program, where we define our Flags
as a type alias, and Elm is able to create a record for us from the JSON we pass in from JS.
Just like an API request, this JSON we give Elm might not match the Flags
type, so Elm gives us an error like this:
Uncaught Error:
Problem with the flags given to your Elm program on initialization.
Problem with the given value: null Expecting an OBJECT with a field named `person`
Using JSON Decoders
If you aren’t familiar with JSON decoders, they tell Elm how to intrepret JSON. The elm/json
package allows us to create decoders, and call decodeValue
, which turns JSON into a Result
(a type that can represent a success or failure)
import Json.Decode as Decode exposing (Decoder)
type alias Person =
{ name : String
, age : Int
}
decoder : Decoder Person
decoder =
Decode.map2 Person
(Decode.field "name" Decode.string)
(Decode.field "age" Decode.int)
viewJsonPerson : Decode.Value -> Html msg
viewJsonPerson value =
case Decode.decodeValue decoder of
Ok person ->
text ("Hello, " ++ person.name ++ "!")
Err reason ->
text (Decode.errorToString reason)
Decoders can be tricky to explain to beginners coming from JS, but once you get the hang of them, the API is really nice.
You can compose them really well into bigger and better things.
decodeValue
returns a Result Error Person
, so it will give us the reason if we want to share that with the user or log it or something cool.
My big question
If Elm already understands how to turn JSON into a Person
using flags, what are the tradeoffs to using that instead of defining decoders?
My oversimplified solution
I understand that flags
do not support custom types or complex data structures, so we would just write functions to turn generic JSON into more complex data values.
Imagining a made-up Json
module like this
module Json exposing (Value, parse)
parse : Value -> Result Error a
-- ... does the magical flag stuff
module Person exposing (Person, fromJson)
import Json
type alias JsonPerson =
{ name : String
, age : Int
, color : String
}
type alias Person =
{ name : String
, age : Int
, color : Color
}
type Color
= Red
| Green
| Blue
fromJson : Json.Value -> Result String Person
fromJson value =
Json.parse value -- Result Error JsonPerson
|> Result.andThen toPerson -- Result String Person
toPerson : JsonPerson -> Result String Person
toPerson person =
colorFromString person.color
|> Maybe.map
(\color -> Person person.name person.age color)
|> Maybe.withDefault
(Err "The `color` value was no good, dood.")
colorFromString : String -> Maybe Color
colorFromString str =
case str of
"red" ->
Just Red
"green" ->
Just Green
"blue" ->
Just Blue
_ ->
Nothing
In this alternate reality, we just write functions to convert from our JSON data type to the one we want to work with in the code.
We use types to describe JSON structure instead of types and decoders, and just like decodeValue
, the JSON we receive might not match up with our types. (This is why Json.parse
would still return a Result
type)
That’s it!
I’m sure there’s an embarassing reason for why this suggestion doesn’t make sense.
Let me know what your thoughts are on this, I’d love to understand more about why decoders might still be the best choice for handling JSON values.