DEV Community

Ken Aguilar
Ken Aguilar

Posted on

4 2

Haskell - Encoding and Decoding JSON with Aeson

There are a lot of blog posts and tutorial about encoding/decoding JSON with aeson. Even the library is pretty good at teaching how to do this. The tutorial I always go back to is Artyom Kazak's tutorial. They talk about lots of different techniques on how to decode and encode json on different cases.

Let's start off with the basics by deriving instances of FromJSON and ToJSON manually.

data Book = Book
  { bookTitle     :: Text
  , bookISBN      :: Text
  , bookPublisher :: Text
  , bookLanguage  :: Text
  } deriving Show

instance FromJSON Book where
  parseJSON = withObject "Book" $ \b ->
    Book <$> b .: "title"
         <*> b .: "ISBN"
         <*> b .: "publisher"
         <*> b .: "language"

instance ToJSON Book where
  toJSON Book {..} = object
    [ "title"     .= bookTitle
    , "ISBN"      .= bookISBN
    , "publisher" .= bookPublisher
    , "language"  .= bookLanguage
    ]

With FromJSON and ToJSON instances we can now consume json of this shape:

{ "title": ".."
, "ISBN": ".."
, "publisher": ".."
, "language": ".."
}

As you can see when the type has more fields that also means that we have to type out all those fields. Your fingers are going to fall off by the time you are done with your app.

One solution to minimize the boilerplate is by using Generics

{-# LANGUAGE DeriveGeneric #-}

import GHC.Generics

data Book = Book
  { bookTitle     :: Text
  , bookISBN      :: Text
  , bookPublisher :: Text
  , bookLanguage  :: Text
  } deriving (Generic, Show)

instance FromJSON Book
instance ToJSON Book

If we use {-# LANGUAGE DeriveAnyClass #-} pragma we can do this.

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}

data Book = Book
  { bookTitle     :: Text
  , bookISBN      :: Text
  , bookPublisher :: Text
  , bookLanguage  :: Text
  } deriving (Generic, Show, FromJSON, ToJSON)

DeriveAnyClass and GenerlizedNewTypeDeriving

If we have both of these language extensions enabled, ghc will complain about derivation being ambigious. To get around this use
{-# LANGUAGE DerivingStrategies #-}
language extension.

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DeriveGeneric #-}

data Book = Book
  { bookTitle     :: Text
  , bookISBN      :: Text
  , bookPublisher :: Text
  , bookLanguage  :: Text
  } 
  deriving (Generic, Show)
  deriving anyclass (FromJSON, ToJSON)

If we don't need to do anything with the field name this will suffice. However, our field name is title not bookTitle so we have to do a little modification to the field names by doing the following

import Text.Casing (camel)
import Data.Aeson

instance FromJSON Book where
  parseJSON = genericParseJSON 
    defaultOptions { fieldLabelModifier = camel . drop 4 }

instance ToJSON Book where
  toJSON = genericToJSON 
    defaultOptions { fieldLabelModifier = camel . drop 4 }

Here's a reference to defaultOptions. In the code above we're doing a record update. That means it's gonna drop 4 characters from the beginning and then camel case it.

Nullable Fields

When it comes to nullable fields, Generics} will automatically use this operator (.:?) on fields that are Maybes, which will use Nothing if
the field is null or missing.

Optional Fields

For optional fields we have to go back to manually deriving ToJSON
and FromJSON manually.

data Book = Book
  { bookTitle     :: Text
  , bookISBN      :: Text
  , bookPublisher :: Text
  , bookLanguage  :: Text
  , bookPrice     :: Maybe (Fixed E2)
  } deriving Show

instance FromJSON Book where
  parseJSON = withObject "Book" $ \b ->
    Book <$> b .: "title"
         <*> b .: "ISBN"
         <*> b .: "publisher"
         <*> b .: "language"
         <*> optional (b .: "price")

Sum Types

{-# LANGUAGE RecordWildCards #-}

data BookFormat 
 = Ebook { price :: Fixed E2 }
 | PhysicalBook { price :: Fixed E2, coverType :: Text }
  deriving Show

instance FromJSON BookFormat where
  parseJSON = withObject "BookFormat" $ \b -> asum
    [ Ebook <$> b .: "price"
    , PhysicalBook <$> b .: "price"
                   <*> b .: "coverType"
    ]

instance ToJSON BookFormat where
  toJSON = \case 
    Ebook {..} -> object [ "price" .= price ]
    PhysicalBook {..} -> object [ "price" .= price, "coverType" .= coverType]

or if we we can decide based on the format

instance FromJSON BookFormat where
  parseJSON = withObject "BookFormat" $ \b -> do
    format <- b .: "format"
    case (format :: Text) of
      "ebook" -> Ebook <$> b .: "price"
      "physicalBook" ->
        PhysicalBook <$> b .: "price"
                     <*> b .: "coverType"

The same with product types we can also use Generics and some
language extensions to derive FromJSON and ToJSON instance

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
import GHC.Generics

data BookFormat
 = Ebook { price :: Fixed E2 }
 | PhysicalBook { price :: Fixed E2, coverType :: Text }
  deriving (Show, Generic, ToJSON, FromJSON)

These are the usual day to day techniques of encoding/decoding json data that I use.

Heroku

Deliver your unique apps, your own way.

Heroku tackles the toil — patching and upgrading, 24/7 ops and security, build systems, failovers, and more. Stay focused on building great data-driven applications.

Learn More

Top comments (0)

AWS Industries LIVE! Stream

Watch AWS Industries LIVE!

New tech. Real solutions. See what’s possible on Industries LIVE! with AWS and AWS Partners.

Learn More

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay