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
= withObject "Book" $ \b ->
parseJSON Book <$> b .: "title"
<*> b .: "ISBN"
<*> b .: "publisher"
<*> b .: "language"
instance ToJSON Book where
Book {..} = object
toJSON "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
= genericParseJSON
parseJSON = camel . drop 4 }
defaultOptions { fieldLabelModifier
instance ToJSON Book where
= genericToJSON
toJSON = camel . drop 4 } defaultOptions { fieldLabelModifier
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 Maybe
s, 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
= withObject "Book" $ \b ->
parseJSON 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
= withObject "BookFormat" $ \b -> asum
parseJSON Ebook <$> b .: "price"
[ PhysicalBook <$> b .: "price"
, <*> b .: "coverType"
]
instance ToJSON BookFormat where
= \case
toJSON Ebook {..} -> object [ "price" .= price ]
PhysicalBook {..} -> object [ "price" .= price, "coverType" .= coverType]
or if we we can decide based on the format
instance FromJSON BookFormat where
= withObject "BookFormat" $ \b -> do
parseJSON <- b .: "format"
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.