Haskell library for deriving and validating JSON Schema (2020-12).
This library provides:
- Automatic JSON Schema derivation from Haskell types via
GHC.Generics. - Sensible encodings for records, products, and sum types (with tags).
$defs/$refsupport for recursive types.- A validator that implements the core 2020-12 validation + applicator vocabularies.
- Helpful error reporting with instance paths when you want detailed feedback.
- High-level API exposed through
Data.JSON.JSONSchema(re-exportingToJSONSchemaand helpers).
- Derive schemas with the
ToJSONSchematype class; generic default handles most ADTs. - Records become JSON objects with named properties, emit
"required"for every field, and forbid extras viaadditionalProperties: false. Non-record products become fixed-length arrays withprefixItems,items: false, andminItems/maxItemspinning the exact length. - Sum types are modeled with discriminator tags:
- Record constructors: object with a required
tag(constructor name) and the record fields. - Non-record constructors: object
{ tag, contents }, both required, wherecontentscarries the constructor’s payload (array/object).
- Record constructors: object with a required
- Recursive types are emitted under
"$defs"and referenced with"$ref". - Validation covers:
type,const,enum, numeric and string constraints, arrays (prefixItems,items,contains,minContains,maxContains), objects (properties,patternProperties,additionalProperties,propertyNames,required,dependentSchemas,dependentRequired), combinators (anyOf,oneOf,allOf,not), conditionals (if/then/else), and pragmaticunevaluated*. - Local
$refresolution using JSON Pointers within the same document.
Notes and limits:
- JSON Schema version: 2020-12.
formatandcontent*are treated as annotations (not asserted). $refresolution is local (#...) only; external URIs/anchors are not resolved.unevaluatedProperties/unevaluatedItemsare implemented with a practical, local approximation.
Add the library to your build, then import the high-level module:
import GHC.Generics (Generic)
import Data.Aeson (ToJSON, Value, object, (.=))
import Data.Proxy (Proxy(..))
import Data.Text (Text)
import Data.JSON.JSONSchema -- ToJSONSchema(..), Proxy(..), validateJSONSchema, validate, validateWithErrors, ValidationError(..)data Person = Person
{ name :: Text
, age :: Int
} deriving (Show, Eq, Generic)
instance ToJSON Person
instance ToJSONSchema Person
-- Produce the JSON Schema (as an Aeson Value)
personSchema :: Value
personSchema = toJSONSchema (Proxy :: Proxy Person)What you get (shape, simplified):
Sum types are tagged. For example:
data Shape
= Circle Double
| Rectangle Double Double
deriving (Show, Eq, Generic)
instance ToJSON Shape
instance ToJSONSchema Shape
shapeSchema :: Value
shapeSchema = toJSONSchema (Proxy :: Proxy Shape)Non-record constructors encode as objects like { tag: { const: "Circle" }, contents: <payload> }.
Record constructors encode as objects with a tag plus their named fields.
Use the simple boolean check:
import Data.Aeson (toJSON)
valid :: Bool
valid = validateJSONSchema personSchema (toJSON (Person "Alice" 30))Or collect all validation errors:
import Data.JSON.JSONSchema (validate, validateWithErrors, ValidationError(..))
case validate personSchema (toJSON (Person "Alice" 30)) of
Right () -> putStrLn "OK"
Left errs -> mapM_ print errs -- includes JSON Pointer-like pathsYou can validate any Value against any schema, including hand-written schemas:
let schema = object
[ "type" .= ("object" :: Text)
, "properties" .= object ["name" .= object ["type" .= ("string" :: Text)]]
, "required" .= (["name"] :: [Text])
]
in validateJSONSchema schema (object ["name" .= ("Bob" :: Text)])When your "types" are runtime values rather than Haskell types — e.g. an
interpreter whose type AST is a single datatype — derivation via ToJSONSchema
does not apply. Instead, fold your value into a Schema and render it; the library
owns the JSON encoding:
import Data.JSON.JSONSchema -- Schema(..), Field(..), AdditionalProperties(..), SchemaDocument(..), schemaToValue, schemaDocumentToValue
userDoc :: SchemaDocument
userDoc = SchemaDocument
{ schemaDefs = [("Code", SString (Just "^[a-z]+$"))] -- a named $def
, schemaRoot = SObject
[ Field "code" True (SRef "Code") -- required, references the $def
, Field "count" False SInteger -- optional
]
APForbidden -- additionalProperties: false
}
userSchema :: Value
userSchema = schemaDocumentToValue userDoc
ok = validateJSONSchema userSchema (object ["code" .= ("abc" :: Text), "count" .= (3 :: Int)]) -- True
bad = validateJSONSchema userSchema (object ["code" .= ("ABC" :: Text)]) -- False (off-pattern)A homogeneous map/dictionary is SObject [] (APSchema valueSchema)
(additionalProperties: <schema>). The pattern field is matched with TDFA
regexes (POSIX ERE), not the full ECMA-262 dialect, so keep patterns simple.
Schema covers the data-shape subset; for any keyword outside it (e.g.
minLength, numeric bounds, title), use the SRaw escape hatch to embed an
arbitrary schema Value at any node — e.g. Field "name" True (SRaw (object ["type" .= ("string" :: Text), "minLength" .= (3 :: Int)])).
Provide an explicit instance when you need a specific schema shape:
newtype UUID = UUID Text
instance ToJSONSchema UUID where
toJSONSchema _ = object
[ "type" .= ("string" :: Text)
, "minLength" .= (36 :: Int)
, "maxLength" .= (36 :: Int)
]- Pretty-print schemas with
aeson-prettyif you want human-friendly output. - For
Maybe a, the schema isanyOf [schema(a), {"type":"null"}]. - For
[a], the schema is{ "type": "array", "items": schema(a) }. - For
Either a b, the schema isanyOfwith{ "Left": a }and{ "Right": b }object encodings.
- Build and test with Cabal:
cabal buildcabal test
Released under the Mozilla Public License 2.0 by DPella AB. See LICENSE for details.
{ "$defs": { "Person": { "type": "object", "properties": { "name": {"type": "string"}, "age": {"type": "integer"} }, "additionalProperties": false, "required": ["name", "age"] } }, "$ref": "#/$defs/Person" }