Haskell Generics + Aeson + QuickCheck For The Win

Recently I’ve been working on an HTTP API written in Haskell. One of the things that tends to be much easier and nicer in dynamic languages is dealing with JSON in these APIs, statically typed languages tend to have lots of boilerplate. Not only is this boilerplate annoying to write but it can cause small changes to the API into a big task.

With Haskell (actually GHC) this is not the case.

A great GHC extension is DeriveGeneric which can be used to define how to generate type class instances. Aeson has made great use of this, lets look at the following example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{-# LANGUAGE DeriveGeneric #-}
module Lib where

import GHC.Generics
import Data.Aeson
import Data.Text

data Person = Person {
name :: Text
, age :: Int
} deriving (Eq, Show, Generic)


instance ToJSON Person

instance FromJSON Person

Here we’ve defined a new record type Person with 2 fields. You can imagine how this would look in JSON:

1
2
3
{ "name": "David"
, "age": 21
}

The instance ToJSON Person automatically creates an instance that will allow the encode function to generate exactly the JSON above. In a similar way, instance FromJSON Person will allow us to decode the above JSON into a Person record. That’s 2 lines of code!

Sometimes we might want to modify the JSON produced slightly, for example in order to generate lenses with TemplateHaskell you add an _ in front of the field names:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{-# LANGUAGE DeriveGeneric   #-}
{-# LANGUAGE TemplateHaskell #-}
module Lib where

import Control.Lens
import Data.Aeson
import Data.Aeson.Types
import Data.Text (Text)
import GHC.Generics

data Person = Person
{ _name :: Text
, _age :: Int
} deriving (Eq, Show, Generic)

makeLenses ''Person

instance ToJSON Person where

toJSON = genericToJSON defaultOptions {fieldLabelModifier = drop 1}

instance FromJSON Person where

parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = drop 1}

That’s not too bad, however now our JSON is a little less trustworthy, especially if we start doing even more customisation of the ToJSON and FromJSON instances. How do we know that the encode/decode functions work correctly?

This is where QuickCheck comes in. Using generic-arbitrary we can create an Arbitrary instance of Person and use QuickCheck to test random samples of Person data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module Main where

import Data.Aeson (FromJSON, ToJSON, decode,
encode)
import Data.Text.Arbitrary ()
import Lib
import Test.Framework (Test, defaultMain)
import Test.Framework.Providers.QuickCheck2 (testProperty)
import Test.QuickCheck.Arbitrary.Generic (Arbitrary, arbitrary,
genericArbitrary,
genericShrink, shrink)

personTest :: Test
personTest = testProperty "Encode and decode a Person" (jsonProperties :: Person -> Bool)

instance Arbitrary Person where

arbitrary = genericArbitrary
shrink = genericShrink

jsonProperties :: (Eq a, ToJSON a, FromJSON a) => a -> Bool
jsonProperties a = Just a == (decode . encode) a

main :: IO ()
main = defaultMain [personTest]

Now we can be pretty confident about the correctness of our JSON conversions.

Until I started using Haskell professionally, I wasn’t totally sold on static typing, however it’s examples like these that have converted me. With as little as 7 lines of code we can be sure that the data we are sending over the wire adheres to our API contract at compile time.