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