Haskell Generic JSON deriving library developed for Flow: Scrive.Aeson.Generic in scrive-commons.
Motivation
A generic Aeson deriving library which:
- Works naturally with sum types
-
Allows encoding existing and new APIs with no manual instances
- properties besides tags in Objects
- Allows storage in the database (good backwards compatibility of encoding)
- Provides good ToSchema instances
Haskell Structures
| Name | Haskell | Aeson | tagged-deriving |
|---|---|---|---|
| Product type (with named fields) |
|
|
|
| Product type (with unnamed fields) |
|
|
|
Sum type |
|
|
|
Sum of products type |
|
no serialization |
? |
Special Cases
| Untagged Newtype |
|
|
| Tagged Newtype |
|
|
| Product type with unnamed fields |
|
Can’t serialize. Aeson-deriving would produce a heterogeneous list: |
Configuration
Configuration structure:
data TaggedOptions = TaggedOptions
{ tagKey :: Maybe Key
-- ^ no tag is needed for product types
, fieldLabelModifier :: String -> String
, constructorTagModifier :: String -> String
, omitNothingFields :: Bool
-- ^ only affects the ToKeyMap, ToJSON instances
}
GTaggedJSON definition:
type GTaggedJSON tagKey = T.GTaggedJSON (TaggedOptions tagKey)
data TaggedOptions (tagKey :: Symbol)
instance (KnownSymbol prefix, KnownSymbol tagKey) => HasTaggedOptions (TaggedOptions tagKey) where
taggedOptions =
TaggedOptions
{ tagKey = fromString . symbolVal $ Proxy @tagKey
, fieldLabelModifier = snakeCase
, constructorTagModifier = snakeCase
, omitNothingFields = True
}
OpenAPI Schemas
TODO
Flow Conventions
- Use prefixes if necessary, strip it in the representation
-
snake_case in JSON, camelCase in Haskell. Capitalize accordingly:
-
HttpUrlSchema->http_url_schema -
HttpURLSchema->http_u_r_l_schema
-
-
Use
Mkprefix for product type constructors when it’s necessary to avoid name clashes with sum type constructors
data Animal
= Person Person
data Person = MkPerson
{ ...
}
- Factor out common fields
| Better | Worse |
|---|---|
|
|
Multiple tags in one object
| Haskell | JSON |
|---|---|
|
|
Use-cases
In Flow, we use TaggedJSON for almost all serializations.
APIs
- Can model most exiting APIs
Database
-
Backwards compatible representation in regards to adding sum constructors, and adding
Maybefields
Downsides
- Your types are informed by the serialization. You sometimes need to do a bit of type juggling.