When we deal with JSON there is always the risk of breaking it, especially if it is edited by humans.
Elm, on the other side, with its encoders/decoders is very strict about it.
Let’s see in this very simple tutorial how Elm can support as in creating a JSON editor.
First we define what is the data structure that we want to store in JSON format
type alias Data =
{ text : String
, logo : Logo
, toggle : Bool
}
text
and toggle
are direct representation of JSON type. logo
is instead an Elm type defined as
type Logo
= Elm
| Strawberry
| Watermelon
Now we need to coder, one to go form JSON to Elm and one to go from Elm to JSON
jsonEncoder : Data -> Json.Encode.Value
jsonEncoder data =
Json.Encode.object
[ ( "text", Json.Encode.string <| data.text )
, ( "logo", Json.Encode.string <| toString data.logo )
, ( "toggle", Json.Encode.bool <| data.toggle )
]jsonDecoder : Json.Decode.Decoder Data
jsonDecoder =
Json.Decode.Pipeline.decode Data
|> Json.Decode.Pipeline.required "text" Json.Decode.string
|> Json.Decode.Pipeline.required "logo" (Json.Decode.string
|> Json.Decode.andThen logoDecoder)
|> Json.Decode.Pipeline.required "toggle" Json.Decode.bool
The decoder for the logo is the more complex because it needs to be translated in an Elm type.
In the encoder this conversion is made just using toString
.
For the decoder we are using Json.Decode.andThen
that create decoders that depend on previous results. So in this case it let the value first be decoded as String. It this is sucesseful, the resulting string is decoded using logoDecoder
.
This is logoDecoder
:
logoDecoder : String -> Json.Decode.Decoder Logo
logoDecoder logoString =
case logoString of
"Elm" ->
Json.Decode.succeed Elm "Strawberry" ->
Json.Decode.succeed Strawberry "Watermelon" ->
Json.Decode.succeed Watermelon _ ->
Json.Decode.fail <| "I don't know a logo named " ++
logoString
Then we need to convert a string to JSON and JSON to a string, something similar to JSON.stringify()
and JSON.parse()
in Javascript.
From data to string the function is simple:
dataToString : Data -> String
dataToString data =
Json.Encode.encode 4 <| jsonEncoder data
4
is the level of indentation in the returning string.
The other way around is a bit more complicated. Let’s do it in two steps.
First we go from String
to Result
:
stringToResult : String -> Result String Data
stringToResult string =
Json.Decode.decodeString jsonDecoder string
Then we go from String
to Data
but at this point we also require the latest version of the Data
so that in case the decoding failed, we return the latest valid data. This way the data will always be in the correct format
stringToData : String -> Data -> ( Maybe String, Data )
stringToData string oldData =
case stringToResult string of
Ok newData ->
( Nothing, newData ) Err err ->
( Just err, oldData )
We also return the error description, in case there was an error while decoding.
To se the behaviour of these decoders/encoders in action is enough to create a textarea with the JSON text and try to edit: http://guupa.com/elm-unbreakable-json/
If our key stroke is bringing the JSON in an impossible state, the app will restore the previous version. (It will also push the cursor at the end of the text, this is a know issue that for the moment doesn’t have a simple solution.)
Thank you for reading.