Schemurai
A small, light-weight-dependency JSON Schema validator for Ruby supporting Draft 7,
Draft 2019-09, and Draft 2020-12. It covers the required cases in the official
JSON-Schema-Test-Suite, as well as the applicable optional tests for numeric
precision, ECMA-262 regular expressions, content validation, anchors, and dynamic
references. The dialect is selected from the schema's $schema URI; schemas that
omit $schema use Draft 7 for compatibility.
require "schemurai"
schema = {
"$schema" => "https://json-schema.org/draft/2020-12/schema",
"type" => "object",
"properties" => {
"order_id" => { "type" => "string", "pattern" => "^ord_[0-9]{6}$" },
"customer" => { "$ref" => "#/$defs/customer" },
"items" => {
"type" => "array",
"minItems" => 1,
"items" => { "$ref" => "#/$defs/line_item" }
},
"created_at" => { "type" => "string", "format" => "date-time" }
},
"required" => %w[order_id customer items created_at],
"additionalProperties" => false,
"$defs" => {
"customer" => {
"type" => "object",
"properties" => {
"id" => { "type" => "integer", "minimum" => 1 },
"name" => { "type" => "string", "minLength" => 1 },
"tier" => { "enum" => %w[standard premium] }
},
"required" => %w[id name],
"additionalProperties" => false
},
"line_item" => {
"type" => "object",
"properties" => {
"sku" => { "type" => "string", "pattern" => "^SKU-[A-Z0-9]{8}$" },
"quantity" => { "type" => "integer", "minimum" => 1 },
"unit_price" => { "type" => "number", "minimum" => 0 }
},
"required" => %w[sku quantity unit_price],
"additionalProperties" => false
}
}
}
order = {
"order_id" => "ord_123456",
"customer" => { "id" => 42, "name" => "Ada Lovelace", "tier" => "premium" },
"items" => [
{ "sku" => "SKU-ABC12345", "quantity" => 2, "unit_price" => 19.95 }
],
"created_at" => "2026-08-31T10:15:00+09:00"
}
validator = Schemurai.compile(schema, format: true)
validator.valid?(order) # => true
invalid_order = order.merge(
"items" => [order.fetch("items").first.merge("quantity" => 0)]
)
result = validator.validate(invalid_order)
result.valid? # => false
result.errors.each do |error|
puts "#{error.instance_path}: #{error.}"
end
For repeated validation, compile the schema once and reuse the validator. A schema registry owns the compiled resource graph, so schemas compiled by the same registry also share their compiled external references.
registry = Schemurai::SchemaRegistry.new(
schemas: {
"https://example.test/positive" => { "type" => "integer", "minimum" => 1 }
}
)
validator = registry.compile({"$ref" => "https://example.test/positive"})
validator.valid?(1) # => true
validator.valid?(0) # => false
validator.validate(0).errors # detailed errors, without recompiling the schema
Schemurai.compile is a convenience for compiling a standalone
validator. Repeatedly compiling the same schema object with one registry reuses
its compiled schema graph. Schemurai.validate and .valid? continue
to accept raw JSON-like schemas and perform compilation internally.
To resolve external references, pass a mapping of URIs to schemas using
schemas:.
Schemurai.valid?(
{ "$ref" => "https://example.test/positive" },
3,
schemas: { "https://example.test/positive" => { "type" => "integer", "minimum" => 1 } }
)
Draft 2019-09 and Draft 2020-12 support their dialect-specific keywords, including
$recursiveRef / $dynamicRef, $defs, dependentSchemas, dependentRequired,
minContains, maxContains, and the unevaluated* applicators. Enable optional
validation for contentEncoding and contentMediaType with content: true.
Enable optional format assertions with format: true; support for each format is
listed separately below.
Thread / Ractor native feature
To share a registry between threads or Ractors, finish registering schemas and make the registry shareable first. This eagerly compiles every registered schema, resolves all references, and makes the registry deeply immutable.
registry = Schemurai::SchemaRegistry.new(
schemas: {
"https://example.test/positive" => { "type" => "integer", "minimum" => 1 },
"https://example.test/value" => { "$ref" => "https://example.test/positive" }
}
)
registry.make_shareable
# Each thread or Ractor creates and owns its validator.
validator = registry.validator_for("https://example.test/value")
make_shareable calls Ractor.make_shareable internally. It raises a
ResolutionError if a reference cannot be resolved. After it returns,
validator_for is read-only and may be called concurrently, while compile is
no longer available. A Validator contains per-validation mutable state and
must not be shared between threads or Ractors.
JSON Schema conformance
| Capability | Draft 7 | Draft 2019-09 | Draft 2020-12 |
|---|---|---|---|
| Required JSON-Schema-Test-Suite cases | Supported | Supported | Supported |
| Dialect-specific references | $ref |
$ref, $recursiveRef |
$ref, $dynamicRef |
unevaluatedItems / unevaluatedProperties |
Not applicable | Supported | Supported |
contentEncoding / contentMediaType assertions |
Opt-in | Opt-in | Opt-in |
The required-suite row covers every required case for the listed dialect in the official JSON Schema Test Suite. The applicable top-level optional cases are also tested, including arbitrary precision numbers, ECMA-262 regular expressions, anchors, cross-draft references, and dynamic references.
Format assertion support
Formats are annotations by default in each standard dialect. Pass format: true
to enable the supported assertions below.[^format]
| Format | Assertion support |
|---|---|
date |
Supported |
time |
Supported |
date-time |
Supported |
duration |
Supported |
email |
Not supported |
idn-email |
Not supported |
hostname |
Not supported |
idn-hostname |
Not supported |
ipv4 |
Supported |
ipv6 |
Supported |
uri |
Not supported |
uri-reference |
Not supported |
iri |
Not supported |
iri-reference |
Not supported |
uuid |
Supported |
uri-template |
Not supported |
json-pointer |
Supported |
relative-json-pointer |
Supported |
regex |
Not supported |
Unsupported and unknown formats remain annotations when assertion is enabled by the caller.
[^content]: Pass content: true to assert Base64 contentEncoding and JSON
contentMediaType. Other encodings and media types remain annotations.
[^format]: A custom Draft 2020-12 meta-schema that declares the Format-Assertion
vocabulary can assert supported formats without the option and rejects
unsupported formats during schema compilation.
Development
Run the test suite with:
bundle exec rspec
Run the linter with:
bundle exec rubocop
AI Disclosure
Large part of this work is generated by OpenAI Codex.