registry-aeson
It's functions all the way down
Presentation
This library is an add-on to registry
, providing customizable encoders / decoders for Aeson.
The approach taken is to add to a registry a list of functions taking encoders / decoders as parameters and producing encoders / decoders.
Then registry
is able to assemble all the functions required to make an Encoder
or a Decoder
of a given type if the encoders or decoders for its dependencies can
be made out of the registry.
By doing so we get all the advantages from using registry
:
- we can override the
aeson
Options
for either a whole graph of data types or just one data type
- we can easily provide a different encodings / decodings for one data type in a specific context (a
Date
can be formatted differently if it is a birth date or an acquisition date for example)
- we can define incremental evolutions of an API, all mapping to the same underlying data model
Encoders
Example
Here is an example of creating encoders for a set of related data types:
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-partial-type-signatures #-}
import Data.Aeson
import Data.Registry
import Data.Registry.Aeson.Encoder
import Data.Time
import Protolude
newtype Identifier = Identifier Int
newtype Email = Email { _email :: Text }
newtype DateTime = DateTime { _datetime :: UTCTime }
data Person = Person { identifier :: Identifier, email :: Email }
data Delivery =
NoDelivery
| ByEmail Email
| InPerson Person DateTime
encoders :: Registry _ _
encoders =
$(makeEncoder ''Delivery)
<: $(makeEncoder ''Person)
<: $(makeEncoder ''Email)
<: $(makeEncoder ''Identifier)
<: fun datetimeEncoder
<: jsonEncoder @Text
<: jsonEncoder @Int
<: defaultEncoderOptions
datetimeEncoder :: Encoder DateTime
datetimeEncoder = fromValue $ \(DateTime dt) -> do
let formatted = toS $ formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ" dt
Object [("_datetime", String formatted)]
In the code above most encoders are created with TemplateHaskell
and the makeEncoder
function. The other encoders are either:
- created manually:
dateTimeEncoder
(note that this encoder needs to be added to the registry with fun
)
- retrieved from a
Aeson
instance: jsonEncoder @Text
, jsonEncoder @Int
Given the list of encoders
an Encoder Person
can be retrieved with:
let encoderPerson = make @(Encoder Person) encoders
let encoded = encodeValue encoderPerson (Person (Identifier 123) (Email "me@here.com")) :: Value
Generated encoders
The makeEncoder
function uses the defaultOptions
added to the registry to produce the same values that a Generic
ToJSON
instance would produce.
NOTE this function does not support recursive data types (and much less mutually recursive data types)
Decoders
Example
Here is an example of creating decoders for a set of related data types:
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-partial-type-signatures #-}
import Data.Aeson
import Data.Registry
import Data.Registry.Aeson.Decoder
import Data.Time
import Protolude
newtype Identifier = Identifier Int
newtype Email = Email { _email :: Text }
newtype DateTime = DateTime { _datetime :: UTCTime }
data Person = Person { identifier :: Identifier, email :: Email }
data Delivery =
NoDelivery
| ByEmail Email
| InPerson Person DateTime
decoders :: Registry _ _
decoders =
$(makeDecoder ''Delivery)
<: $(makeDecoder ''Person)
<: $(makeDecoder ''Email)
<: $(makeDecoder ''Identifier)
<: fun dateTimeDecoder
<: jsonDecoder @Text
<: jsonDecoder @Int
<: defaultDecoderOptions
datetimeDecoder :: Decoder DateTime
datetimeDecoder = Decoder $ \case
String s ->
case parseTimeM True defaultTimeLocale "%Y-%m-%dT%H:%M:%S%QZ" $ toS s of
Just t -> pure (DateTime t)
Nothing -> Left ("cannot read a DateTime: " <> s)
other -> Left $ "not a valid DateTime: " <> show other
In the code above most decoders are created with TemplateHaskell
and the makeDecoder
function. The other decoders are either:
- created manually:
dateTimeDecoder
(note that this decoder needs to be added to the registry with fun
)
- retrieved from a
Aeson
instance: jsonDecoder @Text
, jsonDecoder @Int
Given the list of Decoders
an Decoder Person
can be retrieved with:
let decoderPerson = make @(Decoder Person) decoders
let decoded = decode decoderPerson $ ObjectArray [Number 123, ObjectStr "me@here.com"]
Overriding the generated encoders
There is a bit of flexibility in the way encoders are created with TemplateHaskell.
A custom ConstructorsEncoder
can be added to the registry to tweak the generation:
newtype ConstructorEncoder = ConstructorEncoder
{ encodeConstructor :: Options -> FromConstructor -> (Value, Encoding)
}
A ConstructorEncoder
uses configuration options and type information extracted from
given data type (with TemplateHaskell) in order to produce a Value
and an Encoding
.
If necessary you can provide your own options and reuse the default function to produce different encoders.
Generated decoders
The makeDecoder
function makes the following functions:
-- makeDecoder ''Identifier
\(d::Decoder Int) -> Decoder $ \o -> Identifier <$> decode d o
-- makeDecoder ''Email
\(d::Decoder Text) -> Decoder $ \o -> Email <$> decode d o
-- makeDecoder ''Person
\(d1::Decoder Identifier) (d2::Decoder Email) -> Decoder $ \case
ObjectArray [o1, o2] -> Person <$> decode d1 o1 <*> decode d2 o2
other -> Error ("not a valid Person: " <> show other)
-- makeDecoder ''Delivery
\(d1::Decoder Email) (d2::Decoder Person) (d3::Decoder DateTime) -> Decoder $ \case
ObjectArray [Number 0] -> pure NoDelivery
ObjectArray [Number 1, o1] -> ByEmail <$> decode d1 o1
ObjectArray [Number 2, o1, o2] -> InPerson <$> decode d1 o1 <*> decode d2 o2
other -> Error ("not a valid Delivery: " <> show other)
NOTE this function does not support recursive data types (and much less mutually recursive data types)
Overriding the generated decoders
There is a bit of flexibility in the way decoders are created with TemplateHaskell.
A custom ConstructorsDecoder
can be added to the registry to tweak the generation:
newtype ConstructorsDecoder = ConstructorsDecoder
{ decodeConstructors :: Options -> [ConstructorDef] -> Value -> Either Text [ToConstructor]
}
This function extracts values for a set of constructor definitions and returns ToConstructor
values
containing a JSON Value
to be decoded for each field of a given constructor (along with its name).
If necessary you can provide your own options and reuse the default function to produce different decoders.