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.6"}
]
endQuick 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
endIf 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
enduse 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 :external_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 an external 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:
:external_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)
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(~> 2.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, "~> 2.0"},
{:idna, "~> 6.0 or ~> 7.1"}
]
endGuides
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 --recursiveOr 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 testTest 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.