Skip to content

dpella/jsonschema

Repository files navigation

jsonschema

Hackage

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/$ref support 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-exporting ToJSONSchema and helpers).

Features

  • Derive schemas with the ToJSONSchema type class; generic default handles most ADTs.
  • Records become JSON objects with named properties, emit "required" for every field, and forbid extras via additionalProperties: false. Non-record products become fixed-length arrays with prefixItems, items: false, and minItems/maxItems pinning 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, where contents carries the constructor’s payload (array/object).
  • 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 pragmatic unevaluated*.
  • Local $ref resolution using JSON Pointers within the same document.

Notes and limits:

  • JSON Schema version: 2020-12. format and content* are treated as annotations (not asserted).
  • $ref resolution is local (#...) only; external URIs/anchors are not resolved.
  • unevaluatedProperties/unevaluatedItems are implemented with a practical, local approximation.

Quick Start

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(..)

1) Derive a schema for your type

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):

{
  "$defs": {
    "Person": {
      "type": "object",
      "properties": {
        "name": {"type": "string"},
        "age":  {"type": "integer"}
      },
      "additionalProperties": false,
      "required": ["name", "age"]
    }
  },
  "$ref": "#/$defs/Person"
}

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.

2) Validate data against a schema

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 paths

You 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)])

3) Value-directed construction

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)])).

4) Custom schemas for special types

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)
    ]

Tips

  • Pretty-print schemas with aeson-pretty if you want human-friendly output.
  • For Maybe a, the schema is anyOf [schema(a), {"type":"null"}].
  • For [a], the schema is { "type": "array", "items": schema(a) }.
  • For Either a b, the schema is anyOf with { "Left": a } and { "Right": b } object encodings.

Development

  • Build and test with Cabal:
    • cabal build
    • cabal test

License

Released under the Mozilla Public License 2.0 by DPella AB. See LICENSE for details.

About

JSON Schema derivation and validation

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors