So you’ve learned some basic Haskell and you’re feeling really good about yourself. You sit down the write some code and you’re presented with a deeply nested JSON structure:

{
  "foo": "Hello",
  "bar": 1,
  "baz": "More stuff",
  "people": [
    {
      "name": "Drew",
      "hobbies": [
        { "name": "bridge" },
        { "name": "haskell" }
      ]
    },
    {
      "name": "Jane",
      "hobbies": [
        { "name": "chess" },
        { "name": "ocaml" }
      ]
    }
  ]
}

Your goal is to simply find the name of Drew’s first hobby. LET’S WRITE SOME TYPES!

The Hard Way

First, a sprinkling of language extensions, because Haskell.

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}

module Heavy
  ( main,
  )
where

Next, we’ll import Aeson, Text, and a few other helpers.

import Data.Aeson (FromJSON (..), decode, withObject, (.:))
import Data.ByteString.Lazy (ByteString)
import Data.Foldable (find)
import Data.Text (Text)
import GHC.Generics (Generic)
import Text.RawString.QQ (r)

Next our JSON. We’re using a quasi-quoter to make multi-line strings a bit easier, and we’re using overloaded strings to easily create a ByteString literal.

rawJson :: ByteString
rawJson =
  [r|
    {
      "foo": "Hello",
      "bar": 1,
      "baz": "More stuff",
      "people": [
        {
          "name": "Drew",
          "hobbies": [
            { "name": "bridge" },
            { "name": "haskell" }
          ]
        },
        {
          "name": "Jane",
          "hobbies": [
            { "name": "chess" },
            { "name": "ocaml" }
          ]
        }
      ]
    }
  |]

What next? The “standard” approach is that we define custom types to mirror the structure of our JSON and then attempt to decode the JSON into those types. We can leverage GHC Generics to avoid a lot of boilerplate – except whoops, “name” is used for several keys and Haskell records are a bit unwieldy, so we’ll have to make a custom parser for hobbies.

newtype Hobby = Hobby
  { hobbyName :: Text
  }
  deriving (Eq, Show)

instance FromJSON Hobby where
  parseJSON = withObject "Hobby" $ \o ->
    Hobby <$> o .: "name"

data Person = Person
  { name :: Text,
    hobbies :: [Hobby]
  }
  deriving (Eq, Show, Generic, FromJSON)

newtype Stuff = Stuff
  { people :: [Person]
  }
  deriving (Eq, Show, Generic, FromJSON)

With that all out of the way, we can finally extract that name we care about and print the result.

findHobby :: Maybe Text
findHobby = do
  val <- decode rawJson
  drew <- find ((== "Drew") . name) $ people val

  case hobbies drew of
    [] -> Nothing
    hobby : _ -> pure $ hobbyName hobby

main :: IO ()
main = print findHobby

Let’s run it.

$ stack run
Just "bridge"

This works! It’s quite explicit and we’re making strong statements about the structure of our JSON. That’s great!

Except we literally throw away all of these beautiful types immediately, because we only care about that one little string value. If only there was an easier way.

The Easy Way

This is the fun part of the post where I recommend using lenses within a section titled “The Easy Way”. But honestly, I feel that quickly extracting a value from JSON is one of the best reasons to start exploring lenses as a user. And it’s really easy once if that’s all you’re trying to do! We’ll make use of the lens-aeson library, which is truly a joy.

First, we’ll have a lighter sprinkling of language extensions.

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}

module Light
  ( main,
  )
where

Next, our imports and raw JSON.

import Control.Lens (filtered, preview, view)
import Data.Aeson (Value, decode)
import Data.Aeson.Lens (key, nth, values, _String)
import Data.ByteString.Lazy (ByteString)
import Data.Text (Text)
import Text.RawString.QQ (r)

rawJson :: ByteString
rawJson =
  [r|
    {
      "foo": "Hello",
      "bar": 1,
      "baz": "More stuff",
      "people": [
        {
          "name": "Drew",
          "hobbies": [
            { "name": "bridge" },
            { "name": "haskell" }
          ]
        },
        {
          "name": "Jane",
          "hobbies": [
            { "name": "chess" },
            { "name": "ocaml" }
          ]
        }
      ]
    }
  |]

Note that we’re only pulling in a few helpers from lens-aeson, namely:

  • key to access object keys
  • nth to access elements in an array
  • values to consider all values in an array
  • _String to extract the final string value

We’re also making use of two functions from lens itself:

  • view to actually view the result, returning mempty if the path is bad
  • preview to actually view the result, returning Nothing if the path is bad
  • filtered to only consider a Person with the name “Drew”

Let’s ignore everything about lenses other than the fact that you compose them with . and that they compose left-to-right, the opposite of normal Haskell function composition. Given this, we can view lenses as a kind of “xpath” for digging data out of JSON. To the code!

isDrew :: Value -> Bool
isDrew v = view (key "name" . _String) v == "Drew"

findHobby :: Maybe Text
findHobby = do
  val <- decode rawJson :: Maybe Value

  let path =
        key "people"
          . values
          . filtered isDrew
          . key "hobbies"
          . nth 0
          . key "name"
          . _String

  preview path val

main :: IO ()
main = print findHobby

This time, we decode our JSON directly into a Value type, which will only fail if the JSON is invalid. We then use our lenses to dig into the Value, extracting Drew’s first hobby. Note that because we’re operating on Values, our isDrew function needs the type of Value -> Bool. We use a lens lookup internally to see if the person we’re considering is, in fact, Drew.

But what’s the type of path? Who cares! I refuse to tell you.

Let’s run it.

$ stack run
Just "bridge"

Try Lenses

Next time you just need to get a value out of a deeply-nested JSON object, try lens-aeson. I think you’ll be glad you did.