Benjamin Kovach

Haskell Bits #5 - Easily working with JSON

JSON is ubiquitous nowadays, perhaps most importantly for web APIs. We’ll probably need to interact with (or build) one of those at some point, so we must be able to handle JSON in Haskell, right?

Yep - also it’s pretty easy. Let’s talk about it! First, some boilerplate to get out of the way:

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}

import Control.Lens ((^.), (^?))
import Control.Lens.TH
import Data.Aeson.Lens
import Data.Aeson
import Data.Aeson.Types
import Data.Aeson.TH
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.Text as T
import GHC.Generics

Try not to get too overwhelmed with that giant chunk of imports and extensions. Most of it is only for the template haskell we’ll be using later. The important bit for now is Data.Aeson from the aeson package, which allows us to seamlessly work with JSON. We’ll need lens and lens-aeson packages later on.

Let’s pretend we have some API we’re building and want to generate a static blob of JSON for the front page. How about this:

index :: Value
index = object
    [ "message" .= String "Congrats!"
    , "status" .= String "YOU_GOT_HERE_SO_OBVIOUSLY_SUCCESSFUL"
    , "metadata" .= object [
        "version" .= Number 9
      ]
    ]

Value is the type of JSON values in Haskell. We can build up an object using the object functions, and a mapping from keys to Values, generated by String, Number, and other functions.

We can encode a Value with encode. It produces a lazy ByteString:

BL.putStrLn $ encode index
-- { "status":"YOU_GOT_HERE_SO_OBVIOUSLY_SUCCESSFUL","metadata":{"version":9},"message":"Congrats!"}

Next up, I’d like to show how a client might interact with this thing. If they have the unpacked Value, they can access the value at the message key like this:

index ^. key "message" . _String
-- "Congrats!"

What’s even better is that we don’t even need to unpack the Value! We can operate directly on the encoded JSON:

encode index ^. key "message" . _String
-- "Congrats!"

We can dig into the nested fields safely using ^?:

index ^? key "metadata" . key "app_version" . _Number
-- Just 9.0

All Lens idioms apply. It’s easy to get, set, or modify arbitrary fields of JSON objects this way.

Let’s say we want to start building up our application and require more type safety. aeson makes it easy to generate encoding and decoding mechanisms for your data types. For instance, we can define the following two types, generate lenses (via makeFields) and ToJSON and FromJSON instances using a bit of template haskell:

data Metadata = Metadata
    { _metadataAppVersion :: Int
    } deriving (Show, Eq, Generic)

$(deriveJSON
    defaultOptions
        { fieldLabelModifier = camelTo2 '_' . drop (T.length "_metadata")
        } ''Metadata)
$(makeFields ''Metadata)

data IndexResponse = IndexResponse
    { _indexResponseMessage :: T.Text
    , _indexResponseStatus :: T.Text
    , _indexResponseMetadata :: Metadata
    } deriving (Show, Eq, Generic)

$(deriveJSON
    defaultOptions
        { fieldLabelModifier = camelTo2 '_' . drop (T.length "_indexResponse")
        } ''IndexResponse)
$(makeFields ''IndexResponse)

Here’s the same index structure, now typed more explicitly:

indexResponse :: IndexResponse
indexResponse = IndexResponse
    "Congrats!"
    "YOU_GOT_HERE_SO_OBVIOUSLY_SUCCESSFUL"
    (Metadata 9)
BL.putStrLn $ encode indexResponse
-- {"message":"Congrats!","status":"YOU_GOT_HERE_SO_OBVIOUSLY_SUCCESSFUL","metadata":{"app_version":9}}

Note that the appVersion field gets automatically converted from camelCase to snake_case with the camelTo2 option from Data.Aeson.Types. Handy!

We can check that encoding and decoding works, and use more type-safe lenses (in this case, message, which was generated by makeFields):

(decode (encode indexResponse) :: Maybe IndexResponse) ^. _Just . message
-- "Congrats!"

As you can see, dealing with JSON in Haskell is a breeze! What other tips and tricks do you use when dealing with JSON (de)serialization (in Haskell or otherwise)?

Until next time,

Ben

You can read more about aeson and lens-aeson in the docs: