-
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.
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.
$ 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-bc44d05f24afDecoding, 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.
JValidation is defined as follows:
data JValidation e a =
JValidation (Maybe a) (JValidationReport e)
data JValidationReport e =
JValidationReport [JValidationError e] (Map JPathSegment (JValidationReport e))eis the type of domain-specific errors.jis the validation input (for example,JSON.Value)ais 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 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 usingjObjectDefinition), - then add extra predicates using
jDefinition.
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.
jField uses explicit type applications so that fields would not be mixed
up; makeRecBuilder wraps constructors into something that takes explicitly
named fields.
-
Use
-XDerivingViainstead ofcoerceonce GHC 8.10 is out (due to three-release policy). -
Better document how to use sum type validation. There are tests in
JSON.DefinitionSpecbut no docs yet. -
TODO: comment on
BadExponent. -
Move the
Fieldmachinery intonamed? -
Use something like Barbies or
higgledyto get rid ofFieldand move field names into types?