JSONSchex

hex.pm versionDraft 2020-12

JSONSchex is an implementation of JSON Schema Draft 2020-12 for Elixir, with a design that focuses on practical performance and future support for later specification updates.

Features

Installation

def deps do
[
{:jsonschex, "~> 0.7"}
]
end

Quick start

{:ok, compiled} =
JSONSchex.compile(%{
"type" => "array",
"items" => %{"type" => "integer"}
})
:ok = JSONSchex.validate(compiled, [1, 2, 3])
{:error, errors} = JSONSchex.validate(compiled, [1, "bad"])

Compile-time schemas

If your schema is a static literal known during compilation, JSONSchex supports both an explicit macro API and a compact sigil form.

Using JSONSchex.Schema.compile!/2

You can embed the compiled schema directly in your module with JSONSchex.Schema.compile!/2:

defmodule MyApp.UserSchema do
require JSONSchex.Schema
@schema JSONSchex.Schema.compile!(%{
"type" => "string",
"format" => "email"
}, format_assertion: true)
def schema, do: @schema
end
:ok = JSONSchex.validate(MyApp.UserSchema.schema(), "user@example.com")

Using the ~X sigil

You can also use the ~X sigil from JSONSchex.Sigil for Elixir map literals representing JSON Schemas:

defmodule MyApp.NumberSchema do
use JSONSchex
@schema ~X|%{"type" => "integer", "minimum" => 10}|
def schema, do: @schema
end

If you prefer the explicit form, you can import the sigil directly:

defmodule MyApp.NumberSchema do
import JSONSchex.Sigil, only: [sigil_X: 2]
@schema ~X|%{"type" => "integer", "minimum" => 10}|
def schema, do: @schema
end

use JSONSchex imports the ~X sigil for you. ~X parses Elixir code, not JSON. It currently supports these modifiers:

For compile-time embeddable options such as :loader, prefer remote captures like &MyLoader.fetch/1 over anonymous functions.

~X is preferred over ~J to avoid the common sigil-name conflict with Jason.

How it works

JSONSchex follows a two-phase approach for optimal performance:

  1. Compile — Parse and optimize a JSON Schema into an executable Schema struct. During compilation:

    • All $id, $anchor, and discovered local fragment references are scanned and registered
    • Keywords are compiled into serializable rule descriptors consumed by the validator
    • Remote $ref schemas can be loaded via a configured loader
    • The built-in Draft 2020-12 dialect is recognized without requiring a remote meta-schema load
    • Vocabularies are resolved based on $schema and $vocabulary declarations
  2. Validate — Execute the compiled schema against data. During validation:

    • Rules are executed in order, accumulating errors
    • Evaluated property/item keys are tracked for unevaluatedProperties and unevaluatedItems
    • References ($ref, $dynamicRef) are resolved from the compiled registry
    • All errors are collected and returned together

This design allows you to compile a schema once and reuse it for multiple validations, significantly improving performance for repeated validations.

Error reporting

When validation fails, JSONSchex.validate/2 returns {:error, errors} where errors is a list of JSONSchex.Types.Error structs.

JSONSchex uses a lazy error reporting model for performance. Errors contain raw data (path lists, context maps) rather than pre-formatted strings. You can use JSONSchex.format_error/1 to generate human-readable messages when needed.

Each error contains:

Example:

schema = %{
"type" => "object",
"properties" => %{
"email" => %{"type" => "string", "format" => "email"},
"age" => %{"type" => "integer", "minimum" => 0}
},
"required" => ["email"]
}
{:ok, compiled} = JSONSchex.compile(schema, format_assertion: true)
{:error, errors} = JSONSchex.validate(compiled, %{"age" => -5})
# Inspect raw errors
# [
# %JSONSchex.Types.Error{
# path: ["age"],
# rule: :minimum,
# context: %JSONSchex.Types.ErrorContext{
# contrast: 0,
# input: -5,
# error_detail: nil
# },
# value: -5
# },
# %JSONSchex.Types.Error{
# path: [],
# rule: :required,
# context: %JSONSchex.Types.ErrorContext{
# contrast: ["email"],
# input: nil,
# error_detail: nil
# },
# value: nil
# }
# ]
# Format errors for display
Enum.map(errors, &JSONSchex.format_error/1)
# [
# "At /age: Value -5 is less than minimum 0",
# "Missing required properties: email"
# ]

Compile options

JSONSchex.compile/2 accepts an optional keyword list with the following options:

JSONSchex.compile_fragment/2 accepts a containing document plus an :entry JSON Pointer or URI reference. Use :base_uri when a document-local entry contains relative external references. If :entry includes a base URI/path and :base_uri is omitted, that base is used for relative reference resolution. JSONSchex.bundle_fragment/2 uses the same entry options and returns a raw schema with reachable external resources mounted under $defs. Generated $defs keys beginning with jsonschex_ are collision-safe internal storage names, not stable reference paths; resources retain their identity through $id and anchors. Loader wrapper responses use atom metadata keys only: {:ok, %{document: schema, base_uri: base_uri}}.

JSONSchex.Ref.resolve_selected/2 provides selector-driven $ref resolution for JSON-like documents. It resolves only $ref nodes selected by a caller-provided :select function, replaces selected refs with their target values, preserves unselected refs, continues walking their sibling values, and rebases preserved descendant refs when external content is inlined. It uses the same :base_uri / :loader mechanics for external resources.

Optional Dependencies

JSONSchex has these optional dependencies that enable additional functionality:

To include these dependencies, add them to your mix.exs:

def deps do
[
{:jason, "~> 1.4"},
{:decimal, "~> 3.0"},
{:idna, "~> 6.0 or ~> 7.1"}
]
end

Guides

See the guide/ directory for detailed documentation:

Development

Clone the repository and initialize the git submodules that provide the local test fixtures:

git clone https://github.com/xinz/jsonschex.git
cd jsonschex
git submodule update --init --recursive

Or update git remote submodules in the root directory of this repo:

git submodule update --remote -- test/fixtures/JSON-Schema-Test-Suite && git submodule status -- test/fixtures/JSON-Schema-Test-Suite

This pulls two external test suites into test/fixtures/:

Then fetch dependencies and run the tests:

mix deps.get
mix test

Test suite summary

JSONSchex runs the JSON Schema Test Suite for Draft 2020-12 with all tests passing.

Debug test files can selectively run single suite files for focused investigation.

Benchmark

More benchmark details can be found in the bench/ directory.