schematic
Goal
The goal of the library is to provide a type-safe transport layer for serializing and validating JSON. It can be thought of as a subset of json-schema, which is basically a specification of a JSON document. The other goal is getting as much as possible from this specification for free. Right now the following bits are prototyped:
- All of the instantiations of the transport types structurally follow the schema provided by user
- Serializers are generic, so they follow from the type-level schema and the're supposed to have a roundtrip property by an implementation.
- Runtime value validators are generated from the schema. Validation errors are reported as a pairs of a json-path to the element and an error message.
- There are migrations. It's possible to describe a series of migrations to the schema and have all the necessary machinery to deserialize a user specified version of the schema if there are multiple versions available.
- Schematic schemas can be exported to json-schema
Be aware that library is experimental and subject to change a lot. The current state can be viewed as a prototype.
Installation
- Install Stack
$ stack install schematic
Basic Examples
type SchemaExample
= SchemaObject
'[ '("foo", SchemaArray '[AEq 1] (SchemaNumber '[NGt 10]))
, '("bar", SchemaOptional (SchemaText '[TEnum '["foo", "bar"]]))]
- This schema says that's structurally it's a JSON object which has two fields:
- field "foo" contains an array of numeric values
- field "bar" contains an optional text value
- It also allows you to validate JSON values itself:
- an array in "foo" contains exactly one element
- that element must be strictly greater than 10
- value of "bar" field must be either "foo" or "bar" if presented
This one is valid for example
schemaJson :: ByteString
schemaJson = "{\"foo\": [13], \"bar\": null}"
Also valid
schemaJson :: ByteString
schemaJson = "{\"foo\": [13], \"bar\": \"bar\"}"
it can be parsed and validated like this:
decodeAndValidateJson schemaJson :: ParseResult (JsonRepr SchemaExample)
ParseResult type encodes three possible situations:
- json is structurally consistent with a schema and runtime validations pass
- json source is malformed or doesn't correspond to a schema structurally
- some of the runtime validations have failed, but it's structurally correct overall
It implies a transport layer representation of that data that can be traversed and transformed to whatever internal types user has. It's called JsonRepr. Type parameter is the schema itself.
jsonExample :: JsonRepr SchemaExample
jsonExample = ReprObject $
FieldRepr (ReprArray [ReprNumber 12])
:& FieldRepr (ReprOptional (Just (ReprText "bar")))
:& RNil
Migrations
It's possible to represent schema changes as a series of migrations, which describes a series of json-path/change pairs. Migrations can be applied in succession.
This piece of code will apply a migration to the schema, decode and validate the latest version.
type SchemaExample
= SchemaObject
'[ '("foo", SchemaArray '[AEq 1] (SchemaNumber '[NGt 10]))
, '("bar", SchemaOptional (SchemaText '[TEnum '["foo", "bar"]]))]
type TestMigration =
'Migration "test_revision"
'[ 'Diff '[ 'PKey "bar" ] ('Update ('SchemaText '[]))
, 'Diff '[ 'PKey "foo" ] ('Update ('SchemaNumber '[])) ]
type VS = 'Versioned SchemaExample '[ TestMigration ]
schemaJsonTopVersion :: ByteString
schemaJsonTopVersion = "{ \"foo\": 42, \"bar\": \"bar\" }"
It's possible to decode the latest version like this:
decodeAndValidateJson schemaJsonTopVersion :: ParseResult (JsonRepr (TopVersion (AllVersions VS)))
Lens-compatibility
It's possible to use flens to construct a field lens for a typed object in schematic. Let's suppose you have a schema like this:
type ArraySchema = 'SchemaArray '[AEq 1] ('SchemaNumber '[NGt 10])
type ArrayField = '("foo", ArraySchema)
type FieldsSchema =
'[ ArrayField, '("bar", 'SchemaOptional ('SchemaText '[TEnum '["foo", "bar"]]))]
type SchemaExample = 'SchemaObject FieldsSchema
There are two ways of working with named fields in the objects:
- Using the
fgetandfsetfunctions:fget fooProxy (fput newFooVal objectData) == newFooVal - Using the lens library:
set (flens (Proxy @"foo")) newFooVal objectData ^. flens (Proxy @"foo") == newFooVal
Export to json-schema (draft 4)
TODO
- Overall user friendliness
- Versioned http endpoints