JSONSchex
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
- Implements JSON Schema Draft 2020-12 in full, including all core, applicator, validation, unevaluated, and content vocabulary keywords.
- Passes 100% of the official JSON Schema Test Suite for Draft 2020-12.
- Designed for performance and simplicity: compile a schema once into an executable
Schemastruct, then validate data repeatedly with no repeated parsing overhead.
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.
- Prefer
JSONSchex.Schema.compile!/2when you want the most explicit API. - Prefer
~Xwhen you want a compact module-attribute literal.
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:
f—format_assertion: truec—content_assertion: true
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:
Compile — Parse and optimize a JSON Schema into an executable
Schemastruct. 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
$refschemas 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
$schemaand$vocabularydeclarations
- All
Validate — Execute the compiled schema against data. During validation:
- Rules are executed in order, accumulating errors
- Evaluated property/item keys are tracked for
unevaluatedPropertiesandunevaluatedItems - 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:
path— List of path segments indicating where the error occurred (e.g.,["users", 0, "email"])rule— Atom identifying the failed validation rule (e.g.,:type,:minimum)context— Map containing details about the failure (e.g.,%JSONSchex.Types.ErrorContext{contrast: "integer", input: "string"})value— The input value that caused the error
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:
:loader— Function for loading remote$refschemas (see Loader guide):base_uri— Starting base URI for resolving relative references (see Loader guide):format_assertion— Enable strictformatvalidation (default:false; the built-in Draft 2020-12 dialect keepsformatannotation-only unless explicitly enabled, see Content and format guide):content_assertion— Enable strict content vocabulary validation (default:false, see Content and format guide)
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:
jason(~> 1.4): Required for JSON decoding only when using Elixir earlier than 1.18.decimal(~> 3.0): Required for arbitrary precision decimal validation in themultipleOfkeyword. Without this dependency,multipleOfvalidation may have precision issues with very large or very small decimal numbers.idna(~> 6.0 or ~> 7.1): Required for internationalized domain name (IDN) support. Enables validation ofidn-hostnameandidn-emailformats. Without this dependency, these formats may not be validated in expected ways.
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:
- Loader and remote
$refhandling - Dialect and
$vocabularybehavior - Feature matrix (Draft 2020-12 support)
- Content and format assertion options
- Test suite coverage
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/:
- JSON-Schema-Test-Suite — The official language-agnostic test suite for JSON Schema (Draft 2020-12).
- uritemplate-test — Test cases for RFC 6570 URI Template validation.
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.
- Default suite path:
test/fixtures/JSON-Schema-Test-Suite/tests/draft2020-12
- Exclusions:
optional/cross-draft
Debug test files can selectively run single suite files for focused investigation.
Benchmark
More benchmark details can be found in the bench/ directory.