-
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-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 JValidationError
s 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))
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
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
-XDerivingVia
instead ofcoerce
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 intonamed
? -
Use something like Barbies or
higgledy
to get rid ofField
and move field names into types?