Skip to content

Instantly share code, notes, and snippets.

@cscalfani
Last active November 7, 2017 22:59
Show Gist options
  • Save cscalfani/750d61752de1a82f1560bdc3a0300737 to your computer and use it in GitHub Desktop.
Save cscalfani/750d61752de1a82f1560bdc3a0300737 to your computer and use it in GitHub Desktop.

Type Safe JSON Decoding in Elm

The power of a Static Typed language can seem magical at first. But the goal here is to take a tiny peak behind that curtain.

Elm's implementation of JSON parsing is type safe and how it achieves that can seem like a mystery. Even though I got the code to work, it took me a while to fully understand how it works.

I'm writing it down here for 2 reasons. To help others gain a greater understanding of Types and so I don't forget what I learned.

Word of Caution

Go through this document slowly. There's a lot of new concepts for beginners and you may want to attack this document in multiple sittings. While the code is simple, the concepts are not. I believe that it's worth spending time to get this straight in your head. So please, do yourself a favor and take your time.

Simple Looking Code

So here's the code in its entirety. Don't try to understand it in one big gulp. Below this, is a step by step breakdown.

module JsonTest exposing (..)

import Json.Decode as Json exposing ((:=), maybe, string, int, float)


type alias Address =
    { number : Int
    , street : String
    , city : String
    , state : String
    , zip : String
    }


type alias User =
    { name : String
    , age : Int
    , amount : Float
    , twelve : Int
    , address : Address
    }


json : String
json =
    """
{
    "name": "Joe",
    "age": 20,
    "address": {
        "number": 1234,
        "street" : "Main Street",
        "city": "Jamestown",
        "state": "CA",
        "zip": "99999"
    }
}
"""


(//) : Maybe a -> a -> a
(//) =
    flip Maybe.withDefault


(///) : Json.Decoder a -> a -> Json.Decoder a
(///) decoder default =
    (maybe decoder) `Json.andThen` (\maybe -> Json.succeed (maybe // default))


(<||) : Json.Decoder (a -> b) -> Json.Decoder a -> Json.Decoder b
(<||) =
    Json.map2 (<|)


addressDecoder : Json.Decoder Address
addressDecoder =
    Json.succeed Address
        <|| ("number" := int)
        <|| ("street" := string)
        <|| ("city" := string)
        <|| ("state" := string)
        <|| ("zip" := string)


userDecoder : Json.Decoder User
userDecoder =
    Json.succeed User
        <|| ("name" := string)
        <|| ("age" := int)
        <|| (("amount" := float) /// 100)
        <|| Json.succeed 12
        <|| ("address" := addressDecoder)


user : Result String User
user =
    Debug.log "user" <| Json.decodeString userDecoder json

The Breakdown

Module and Imports

module JsonTest exposing (..)

import Json.Decode as Json exposing ((:=), maybe, string, int, float)

This code defines the module and imports the JSON Decoding module from the core Elm libraries. It renames Json.Decode to Json because it's less verbose. It also exposes some of the functions we'll be using.

Types Aliases

type alias Address =
    { number : Int
    , street : String
    , city : String
    , state : String
    , zip : String
    }


type alias User =
    { name : String
    , age : Int
    , amount : Float
    , twelve : Int
    , address : Address
    }

These are some example type aliases, viz. a User and their Address. So note that this is a hierarchical structure. So we'll expect our JSON to have a key with a value that is another object.

JSON

json : String
json =
    """
{
    "name": "Joe",
    "age": 20,
    "address": {
        "number": 1234,
        "street" : "Main Street",
        "city": "Jamestown",
        "state": "CA",
        "zip": "99999"
    }
}
"""

And sure enough our expectations have been met. Notice that address is an object.

One thing to notice here is the triple quotes """. In Elm, this allows us to have multi-line strings and it allows us to embed double quotes without having to escape, i.e. there is no need for "\"".

Maybe Helper

(//) : Maybe a -> a -> a
(//) =
    flip Maybe.withDefault

This is just a convenience operator because I don't like the verbosity of Maybe.withDefault. This operator is normally for Integer division where the remainder is discarded. But I almost never do that, so as far as I'm concerned this operator is up for grabs (at least in this module where there's no math).

This is also familiar to Perl programmers for define-or operator. And it's just a slanted version of idiomatic Javascript for default values, e.g.

const f = (a, b) => {
    a = a || 0;
    b = b || 0;
    return a + b;
}

Also notice the flip function. If we look at this function's implementation we'll see:

flip : (a -> b -> c) -> (b -> a -> c)
flip f a b=
    f b a

The flip function does exactly what it's name implies, it flips the first 2 parameters of any function.

This was employed on Maybe.withDefault because it had the parameters in the wrong order for this operator. The default needs to be the second parameter for this to work as hoped, which is:

notMaybe = maybe // default

Maybe Decoder Helper

In the same vein, we have a version for decoders:

(///) : Json.Decoder a -> a -> Json.Decoder a
(///) decoder default =
    (maybe decoder) `Json.andThen` (\maybe -> Json.succeed (maybe // default))

This allows us to concisely write a decoder with defaults. First the decoder of some type a is "maybe-fied" with (maybe decoder). So now we can write the decoder as if it's not a maybe. Then we just provide a default of same type a and we get back a decoder that takes the value returned from the maybe decoder and wraps it in a Json.succeed with a default.

We can see how it's used in the userDecoder:

userDecoder : Json.Decoder User
userDecoder =
    Json.succeed User
        <|| ("name" := string)
        <|| ("age" := int)
        <|| (("amount" := float) /// 100)
        <|| Json.succeed 12
        <|| ("address" := addressDecoder)

This is nice and concise.

Decoder Helper

Next we have:

(<||) : Json.Decoder (a -> b) -> Json.Decoder a -> Json.Decoder b
(<||) =
    Json.map2 (<|)

Now this function had me baffled when I first saw an implementation in a blog that credited Evan Czaplicki, the creator of Elm. My version differs from the blog in that it uses an infixed operator because I think it makes the code look nicer as we will see later. I also made this one point-free.

So let's breakdown this function slowly to see how it works.

Let's look first at the Type Signature. To understand how it goes from Json.map2's signature to this one, we should take the implementation apart one step at a time.

First, let's recall Json.map2's signature:

map2 : (a -> b -> c) -> Json.Decoder a -> Json.Decoder b -> Json.Decoder c

Next let's look at:

(<||) : Json.Decoder (a -> b) -> Json.Decoder a -> Json.Decoder b
(<||) =
    Json.map2 (<|)

This is where I got confused. I stared at this signature for longer than I'd like to admit before I realized what was happening.

How does simply adding (<|) as the first parameter of Json.map2 change the signature so drastically?

Here's the key, a, b and c in map2 have nothing to do with a and b in (<||). Once I realized that my thinking was clouded by the use of the same names, I eventually saw how (<|) transformed the type signature.

Thinking positionally, and substituting (<|)'s signature, (a -> b -> c), for map2's first parameter, we get:

map2 :  (   a     -> b -> c) -> Json.Decoder    a     -> Json.Decoder b -> Json.Decoder c
map2' : ((a -> b) -> a -> b) -> Json.Decoder (a -> b) -> Json.Decoder a -> Json.Decoder b

Notice that the everywhere a is in map2, (a -> b) is substituted in map2'.

And where b is in map2, a is substituted in map2'. And c in map2 is b in map2'.

Once map2' gets it's first parameter, viz. (<|), it's signature is the same as (<||)'s:

(<||) : Json.Decoder (a -> b) -> Json.Decoder a -> Json.Decoder b
(<||) =
    Json.map2 (<|)

So now that we understand the signature, what does this function do?

Well, to understand that, we have to look ahead in the code to see how it's used. Let's look at the Decoders.

Decoders

addressDecoder : Json.Decoder Address
addressDecoder =
    Json.succeed Address
        <|| ("number" := int)
        <|| ("street" := string)
        <|| ("city" := string)
        <|| ("state" := string)
        <|| ("zip" := string)


userDecoder : Json.Decoder User
userDecoder =
    Json.succeed User
        <|| ("name" := string)
        <|| ("age" := int)
        <|| (("amount" := float) /// 100)
        <|| Json.succeed 12
        <|| ("address" := addressDecoder)

First notice that the decoders both start with Json.succeed <constructor>, where <constructor> is Address in addressDecoder and User in userDecoder.

Lets look at these parts:

type alias Address =
    { number : Int
    , street : String
    , city : String
    , state : String
    , zip : String
    }

succeed : a -> Json.Decoder a

z : Json.Decoder (Int -> String -> String -> String -> String -> Address)
z =
    Json.succeed Address

Notice that the constructor Address's signature, Int -> String -> String -> String -> String is in the order of the items in the record.

Also, notice that succeed's signature has an a as the first parameter. This parameter was replaced in z with Address's signature since the Address constructor was passed as it's one and only parameter.

A quick reminder that signatures are parenthesized on the right:

z : Json.Decoder (Int -> (String -> (String -> (String -> (String -> Address)))))
z =
    Json.succeed Address

That fact will help us moving forward.

Now, let's look at each step of the addressDecoder. I'm going to cheat and show the generic signature for (<||) for reference and then the substitution second.

This code is wide so you may need to scroll.

(<||) : Json.Decoder (a   ->               b                                        ) -> Json.Decoder a   -> Json.Decoder                          b
(<||) : Json.Decoder (Int -> (String -> (String -> (String -> (String -> Address))))) -> Json.Decoder Int -> Json.Decoder (String -> (String -> (String -> (String -> Address))))

step1 : Json.Decoder (String -> (String -> (String -> (String -> Address))))
step1 =
    Json.succeed Address
        <|| ("number" := int)

So step1 effectively "peeled off" the first type, Int. Now step2:

(<||) : Json.Decoder (a      ->                 b                          ) -> Json.Decoder a      -> Json.Decoder                 b
(<||) : Json.Decoder (String -> (String -> (String -> (String -> Address)))) -> Json.Decoder String -> Json.Decoder (String -> (String -> (String -> Address)))

step2 : Json.Decoder (String -> (String -> (String -> Address)))
step2 =
    Json.succeed Address
        <|| ("number" := int)
        <|| ("age" := int)

And each step will continue:

(<||) : Json.Decoder (a      ->               b                ) -> Json.Decoder a      -> Json.Decoder               b
(<||) : Json.Decoder (String -> (String -> (String -> Address))) -> Json.Decoder String -> Json.Decoder (String -> (String -> Address))

step3 : Json.Decoder (String -> (String -> Address))
step3 =
    Json.succeed Address
        <|| ("number" := int)
        <|| ("age" := int)
        <|| ("city" := string)
(<||) : Json.Decoder (a      ->         b          ) -> Json.Decoder a      -> Json.Decoder         b
(<||) : Json.Decoder (String -> (String -> Address)) -> Json.Decoder String -> Json.Decoder (String -> Address)

step4 : Json.Decoder (String -> Address)
step4 =
    Json.succeed Address
        <|| ("number" := int)
        <|| ("age" := int)
        <|| ("city" := string)
        <|| ("state" := string)
(<||) : Json.Decoder (a      ->    b   ) -> Json.Decoder a      -> Json.Decoder    b
(<||) : Json.Decoder (String -> Address) -> Json.Decoder String -> Json.Decoder Address

step5 : Json.Decoder Address
step5 =
    Json.succeed Address
        <|| ("number" := int)
        <|| ("age" := int)
        <|| ("city" := string)
        <|| ("state" := string)
        <|| ("zip" := string)

Notice that step5 is equivalent to addressDecoder. Each time we add another decoder for the next record item, we reduce the signature until we finally get a Json.Decoder Address.

The best part about the way this works is that it's Type Safe!!!

And if we look at the decoders again:

addressDecoder : Json.Decoder Address
addressDecoder =
    Json.succeed Address
        <|| ("number" := int)
        <|| ("street" := string)
        <|| ("city" := string)
        <|| ("state" := string)
        <|| ("zip" := string)


userDecoder : Json.Decoder User
userDecoder =
    Json.succeed User
        <|| ("name" := string)
        <|| ("age" := int)
        <|| (("amount" := float) /// 100)
        <|| Json.succeed 12
        <|| ("address" := addressDecoder)

We can see that they nest beautifully viz. the last decoder in userDecoder is the addressDecoder.

And finally the bit of test code:

user : Result String User
user =
    Debug.log "user" <| Json.decodeString userDecoder json

This displays the following:

$ elm repl
---- elm-repl 0.17.1 -----------------------------------------------------------
 :help for help, :exit to exit, more at <https://github.com/elm-lang/elm-repl>
--------------------------------------------------------------------------------
> import JsonTest
user: Ok { name = "Joe", age = 20, amount = 100, twelve = 12, address = { number = 1234, street = "Main Street", city = "Jamestown", state = "CA", zip = "99999" } }
    :
>

Conclusion

I hope this article helps illuminate many things that maybe were a bit fuzzy or you didn't know existed. I feel I really learned a lot by analyzing exactly what was going on here. I hope you feel the same.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment