robb.re

Json with Aeson

Posted on July 23, 2015

In general, using Aeson makes encoding types as json incredibly straightforward

data Person = Person String Int
instance ToJSON Person where
    toJSON (Person name age) = object ["name" .= name, "age" .= age]

In ghci we can test this

λ> encode $ Person "Noah" 950
"{\"age\":950,\"name\":\"Noah\"}"

If we enable DeriveGeneric then this gets even simpler

{-# LANGUAGE OverloadedStrings, DeriveGeneric #-}
data Person = Person String Int deriving Generic
instance ToJSON Person

What was not apparent to me was how to create differently structured responses and I couldn’t find any documented examples. In the Haskell world the refrain is typically “Just read the types”. For me, that wasn’t helpful. Luckily the type definitions on hackage always have a link to the source code and that was very helpful. After digging through the source I realised how the types did actually tell me everything I needed to know; I just didn’t know how to read the signs. Anyway, here’s what I put together.

What I wanted to create was a jsonapi.org style object like this

{"type": "people",
 "id": "1",
 "attributes": {
        "age": 950,
        "name": "Noah"
    }
}

The definition of toJson is

class ToJSON a where
    toJSON   :: a -> Value

and Value looks like this

-- | A JSON value represented as a Haskell value.
data Value = Object !Object
        | Array !Array
        | String !Text
        | Number !Number
        | Bool !Bool
        | Null
            deriving (Eq, Show, Typeable)

Alongside the Object constructor there is also an Object type synonym

-- | A JSON \"object\" (key\/value map).
type Object = HashMap Text Value

To create the inner attributes object we can just use Aeson’s object helper to create a Value from Object

λ> object ["name" .= "Noah"]
Object (fromList [("name",String "Noah")])
λ> :t object ["name" .= "Noah"]
object ["name" .= "Noah"] :: Value

So if we create the attributes object and insert it into a HashMap we’ll get back a valid Object type that we can use to create a Value via the Object constructor

λ> import qualified Data.HashMap.Strict as H
λ> let attrs = object ["name" .= "Noah"]
λ> :t H.singleton ("attributes"::T.Text) attrs
H.singleton ("attributes"::T.Text) attrs :: H.HashMap T.Text Value

(to be precise, we’ll get back HashMap Text Value not Object but remember that Object is a synonym for HashMap Text Value).

We can see that using the Object constructor with our Object type we can create a Value which is what we need to return from our toJSON function.

λ> :t Object $ H.singleton ("attributes"::T.Text) attrs
Object $ H.singleton ("attributes"::T.Text) attrs :: Value

So from here we can put it together as follows

instance ToJSON Person where
    toJSON (Person name age) = do
        let attrs = object ["name" .= name, "age" .= age]
        Object $ H.singleton "attributes" attrs