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 exactly one of :entry_pointer or :entry_ref. Use :base_uri when an :entry_pointer fragment contains relative external references. If :entry_ref 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 entrypoint options and returns a raw schema with reachable external resources mounted under $defs. 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 unchanged, and 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.