Bidirectional JSON serialization
Go to file
Vladislav Zavialov 9b76537ad1 GitHub Actions CI
2021-03-21 17:03:52 +03:00
.github/workflows GitHub Actions CI 2021-03-21 17:03:52 +03:00
lib/Jijo Clean-up and some documentation 2019-12-24 01:48:40 +03:00
test Check for extra fields 2019-11-26 22:09:14 +03:00
.gitignore Initial commit 2019-06-18 12:59:40 +03:00
ChangeLog.md Changelog and license 2019-06-18 14:34:12 +03:00
jijo.cabal GitHub Actions CI 2021-03-21 17:03:52 +03:00
LICENSE Fix license date range 2019-07-02 14:42:43 +03:00
README.md GitHub Actions CI 2021-03-21 17:03:52 +03:00

jijo: Bidirectional JSON serialization

Build Status

Design Goals

  • Explicitness decouple types and encoders/decoders (unlike autoderived instances in Aeson).

  • Bidirectionality use the same definition for encoding and decoding to prevent mistakes when one side of the definition is updated and the other is not.

  • Completeness collect as many validation errors as possible, instead of stopping after the first error.

Module Structure

Core:

  • JSON.Definition the core of the framework, includes combinators for defining complete JSON definitions, parsing primitives, objects, sums, and adding predicates to validate complex conditions.

  • JSON.Validation the validation machinery, complex enough to deserve its own module.

  • JSON.Path utilities for working with JSONPath, which is used for error reporting.

Records:

  • RecordField.* helpers for generating record constructors that make it harder to mix up fields when decoding records from JSON.

An Example

$ stack repl

Encoding:

> uuid <- Data.UUID.V4.nextRandom

> encodeViaDefinition jUUID uuid
String "c7d63bec-517b-48d8-b77a-bc44d05f24af"

Decoding, happy path:

> import Data.Aeson

> validateViaDefinition jUUID (String "c7d63bec-517b-48d8-b77a-bc44d05f24af")
Right c7d63bec-517b-48d8-b77a-bc44d05f24af

Decoding, type mismatch:

> validateViaDefinition jUUID (Number 42)
Left (JValidationReport [JTypeNotOneOf (fromList [JTyString])] (fromList []))

Decoding, malformed UUID:

> validateViaDefinition jUUID (String "invalid")
Left (JValidationReport [JValidationFail InvalidUUID] (fromList []))

The errors are returned as a prefix tree of JValidationErrors indexed by JPathSegment. They can include domain-specific errors.

Implementation Details

JValidation

JValidation is defined as follows:

data JValidation e a =
  JValidation (Maybe a) (JValidationReport e)

data JValidationReport e =
  JValidationReport [JValidationError e] (Map JPathSegment (JValidationReport e))
  • e is the type of domain-specific errors.
  • j is the validation input (for example, JSON.Value)
  • a is the validation result.

The Applicative instance for JValidation accumulates errors from all subcomputations. We don't want to have a Monad instance for JValidation because it would violate the (<*>) = ap law.

JDefinition

JDefinition is a categorical (arrow) product of a validator and an encoder:

type JDefinition e = ArrPair (ValidationArr e) EncodingArr

data ArrPair p q j a = ArrPair (p j a) (q j a)

newtype ValidationArr e j a =
  ValidationArr (j -> JValidation e a)

newtype EncodingArr j a =
  EncodingArr (a -> j)

It has a Category instance that can be used for sequential/monadic validation: any failed step of the pipeline aborts the pipeline. In most cases, a JDefinition can be built by using the same recipe:

  • narrow down the type using one of existing primitive combinators
  • (jString, jObject, etc), parse (probably using jObjectDefinition),
  • then add extra predicates using jDefinition.

JObjectDefinition

JObjectDefinition is an applicative Product of a validator and an encoder. It can be converted into a JDefinition. It does not have a Monad instance but it can be used for "parallel" applicative validation all errors will be reported in parallel.

makeRecBuilder

jField uses explicit type applications so that fields would not be mixed up; makeRecBuilder wraps constructors into something that takes explicitly named fields.

Future Work

  • Use -XDerivingVia instead of coerce once GHC 8.10 is out (due to three-release policy).

  • Better document how to use sum type validation. There are tests in JSON.DefinitionSpec but no docs yet.

  • TODO: comment on BadExponent.

  • Move the Field machinery into named?

  • Use something like Barbies or higgledy to get rid of Field and move field names into types?