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.message}"
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.