JSON
You will be sending lots of JSON in your programs. You use the Json.Decode
library to convert wild and crazy JSON into nicely structured Elm values.
The core concept for working with JSON is called a decoder. It is a value that knows how to turn certain JSON values into Elm values. We will start out by looking at some very basic decoders (how do I get a string?) and then look at how to put them together to handle more complex scenarios.
Primitive Decoders
Here are the type signatures for a couple primitive decoders:
string : Decoder String
int : Decoder Int
float : Decoder Float
bool : Decoder Bool
These become useful when paired with the decodeString
function:
decodeString : Decoder a -> String -> Result String a
This means we can do stuff like this:
> import Json.Decode exposing (..)
> decodeString int "42"
Ok 42 : Result String Int
> decodeString float "3.14159"
Ok 3.14159 : Result String Float
> decodeString bool "true"
Ok True : Result String Bool
> decodeString int "true"
Err "Expecting an Int but instead got: true" : Result String Int
So our little decoders let us turn strings of JSON values into a Result
telling us how the conversion went.
Now that we can handle the simplest JSON values, how can we deal with more complex things like arrays and objects?
Combining Decoders
The cool thing about decoders is that they snap together building blocks. So if we want to handle a list of values, we would reach for the following function:
list : Decoder a -> Decoder (List a)
We can combine this with all the primitive decoders now:
> import Json.Decode exposing (..)
> int
<decoder> : Decode Int
> list int
<decoder> : Decode (List Int)
> decodeString (list int) "[1,2,3]"
Ok [1,2,3] : Result String (List Int)
> decodeString (list string) """["hi", "yo"]"""
Ok ["hi", "yo"] : Result String (List String)
So now we can handle JSON arrays. If we want to get extra crazy, we can even nest lists.
> decodeString (list (list int)) "[ [0], [1,2,3], [4,5] ]"
Ok [[0],[1,2,3],[4,5]] : Result String (List (List Int))
Decoding Objects
Decoding JSON objects is slightly fancier than using the list
function, but it is the same idea. The important functions for decoding objects is an infix operator:
(:=) : String -> Decode a -> Decode a
This says "look into a given field, and try to decode it in a certain way". So using it looks like this:
> import Json.Decode exposing (..)
> "x" := int
<decoder> : Decode Int
> decodeString ("x" := int) """{ "x": 3, "y": 4 }"""
Ok 3 : Result String Int
> decodeString ("y" := int) """{ "x": 3, "y": 4 }"""
Ok 4 : Result String Int
That is great, but it only works on one field. We want to be able to handle objects larger than that, so we need help from functions like this:
object2 : (a -> b -> value) -> Decoder a -> Decoder b -> Decoder value
This function takes in two different decoders. If they are both successful, it uses the given function to combine their results. So now we can put together two different decoders:
> import Json.Decode exposing (..)
> (,)
<function> : a -> b -> (a, b)
> point = object2 (,) ("x" := int) ("y" := int)
<decoder> : Decode (Int, Int)
> decodeString point """{ "x": 3, "y": 4 }"""
Ok (3,4) : Result String (Int, Int)
There are a bunch of functions like object2
(like object3
and object4
) for handling different sized objects.
Note: Later we will see tricks so you do not need a different function depending on the size of the object you are dealing with. You can also use functions like
dict
andkeyValuePairs
if the JSON you are processing is using an object more like a dictionary.
Optional Fields
By now we can decode arbitrary objects, but what if that object has an optional field? Now we want the maybe
function:
maybe : Decoder a -> Decoder (Maybe a)
It is saying, try to use this decoder, but it is fine if it does not work.
> import Json.Decode exposing (..)
> type alias User = { name : String, age : Maybe Int }
> user = object2 User ("name" := string) (maybe ("age" := int))
<decoder> : Decode User
> decodeString user """{ "name": "Tom", "age": 42 }"""
Ok { name = "Tom", age = Just 42 } : Result String User
> decodeString user """{ "name": "Sue" }"""
Ok { name = "Sue", age = Nothing } : Result String User
Weirdly Shaped JSON
There is also the possibility that a field can hold different types of data in different scenarios. I have seen a case where a field is usually an integer, but sometimes it is a string holding a number. I am not naming names, but it was pretty lame. Luckily, it is not too crazy to make a decoder for this situation as well. The two functions that will help us out are oneOf
and customDecoder
:
oneOf : List (Decoder a) -> Decoder a
customDecoder : Decoder a -> (a -> Result String b) -> Decoder b
The oneOf
function takes a list of decoders and tries them all until one works. If none of them work, the whole thing fails. The customDecoder
function runs a decoder, and if it succeeds, does whatever further processing you want. So the solution to our "sometimes an int, sometimes a string" problem looks like this:
sillyNumber : Decoder Int
sillyNumber =
oneOf
[ int
, customDecoder string String.toInt
]
We first try to just read an integer. If that fails, we try to read a string and then convert it to an integer with String.toInt
. In your face crazy JSON!
Broader Context
By now you have seen a pretty big chunk of the actual Json.Decode
API, so I want to give some additional context about how this fits into the broader world of Elm and web apps.
Validating Server Data
The conversion from JSON to Elm doubles as a validation phase. You are not just converting from JSON, you are also making sure that JSON conforms to a particular structure.
In fact, decoders have revealed weird data coming from NoRedInk's backend code! If your server is producing unexpected values for JavaScript, the client just gradually crashes as you run into missing fields. In contrast, Elm recognizes JSON values with unexpected structure, so NoRedInk gives a nice explanation to the user and logs the unexpected value. This has actually led to some patches in Ruby code!
A General Pattern
JSON decoders are actually an instance of a more general pattern in Elm. You see it whenever you want to wrap up complicated logic into small building blocks that snap together easily. Other examples include:
Random
— TheRandom
library has the concept of aGenerator
. So aGenerator Int
creates random integers. You start with primitive building blocks that generate randomInt
orBool
. From there, you use functions likemap
andandMap
to build up generators for fancier types.Easing
— The Easing library has the concept of anInterpolation
. AnInterpolation Float
describes how to slide between two floating point numbers. You start with interpolations for primitives likeFloat
orColor
. The cool thing is that these interpolations compose, so you can build them up for much fancier types.
As of this writing, there is some early work on Protocol Buffers (binary data format) that uses the same pattern. In the end you get a nice composable API for converting between Elm values and binary!