A high-performance JSON Schema validator for Ruby.
require 'jsonschema_rs'
schema = { "maxLength" => 5 }
instance = "foo"
# One-off validation
JSONSchema.valid?(schema, instance) # => true
begin
JSONSchema.validate!(schema, "incorrect")
rescue JSONSchema::ValidationError => e
puts e.message # => "\"incorrect\" is longer than 5 characters"
end
# Build & reuse (faster)
validator = JSONSchema.validator_for(schema)
# Iterate over errors
validator.each_error(instance) do |error|
puts "Error: #{error.message}"
puts "Location: #{error.instance_path}"
end
# Boolean result
validator.valid?(instance) # => true
# Structured output (JSON Schema Output v1)
evaluation = validator.evaluate(instance)
evaluation.errors.each do |err|
puts "Error at #{err[:instanceLocation]}: #{err[:error]}"
endMigrating from
json_schemer? See the migration guide.
- 📚 Full support for popular JSON Schema drafts
- 🌐 Remote reference fetching (network/file)
- 🔧 Custom keywords and format validators
- ✨ Meta-schema validation for schema documents
- 📦 Schema bundling into Compound Schema Documents
♦️ Supports Ruby 3.2, 3.4 and 4.0
The following drafts are supported:
You can check the current status on the Bowtie Report.
Add to your Gemfile:
gem 'jsonschema_rs'
Pre-built native gems are available for:
- Linux:
x86_64,aarch64(glibc and musl) - macOS:
x86_64,arm64 - Windows:
x64(mingw-ucrt)
If no pre-built gem is available for your platform, it will be compiled from source during installation. You'll need:
- Ruby 3.2+
- Rust toolchain (rustup)
For validating multiple instances against the same schema, create a reusable validator.
validator_for automatically detects the draft version from the $schema keyword in the schema and falls back to Draft 2020-12:
validator = JSONSchema.validator_for({
"type" => "object",
"properties" => {
"name" => { "type" => "string" },
"age" => { "type" => "integer", "minimum" => 0 }
},
"required" => ["name"]
})
validator.valid?({ "name" => "Alice", "age" => 30 }) # => true
validator.valid?({ "age" => 30 }) # => falseYou can use draft-specific validators for different JSON Schema versions:
validator = JSONSchema::Draft7Validator.new(schema)
# Available: Draft4Validator, Draft6Validator, Draft7Validator,
# Draft201909Validator, Draft202012Validatorphone_format = ->(value) { value.match?(/^\+?[1-9]\d{1,14}$/) }
validator = JSONSchema.validator_for(
{ "type" => "string", "format" => "phone" },
validate_formats: true,
formats: { "phone" => phone_format }
)class EvenValidator
def initialize(parent_schema, value, schema_path)
@enabled = value
end
def validate(instance)
return unless @enabled && instance.is_a?(Integer)
raise "#{instance} is not even" if instance.odd?
end
end
validator = JSONSchema.validator_for(
{ "type" => "integer", "even" => true },
keywords: { "even" => EvenValidator }
)Each custom keyword class must implement:
initialize(parent_schema, value, schema_path)- called during schema compilationvalidate(instance)- raise on failure, return normally on success
When validate raises, the original exception is preserved as the cause of the ValidationError, so callers can inspect it:
begin
validator.validate!(3)
rescue JSONSchema::ValidationError => e
puts e.cause.class # => RuntimeError
puts e.cause.message # => "3 is not even"
endWhen you need more than a boolean result, use the evaluate API to access the JSON Schema Output v1 formats:
schema = {
"type" => "object",
"properties" => {
"name" => { "type" => "string" },
"age" => { "type" => "integer" }
},
"required" => ["name"]
}
validator = JSONSchema.validator_for(schema)
evaluation = validator.evaluate({ "age" => "not_an_integer" })
evaluation.valid? # => falseFlag output — simplest, just valid/invalid:
evaluation.flag
# => {valid: false}List output — flat list of all evaluation nodes:
evaluation.list
# => {valid: false,
# details: [
# {valid: false, evaluationPath: "", schemaLocation: "", instanceLocation: ""},
# {valid: true, evaluationPath: "/type", schemaLocation: "/type", instanceLocation: ""},
# {valid: false, evaluationPath: "/required", schemaLocation: "/required",
# instanceLocation: "",
# errors: {"required" => "\"name\" is a required property"}},
# {valid: false, evaluationPath: "/properties", schemaLocation: "/properties",
# instanceLocation: "", droppedAnnotations: ["age"]},
# {valid: false, evaluationPath: "/properties/age", schemaLocation: "/properties/age",
# instanceLocation: "/age"},
# {valid: false, evaluationPath: "/properties/age/type",
# schemaLocation: "/properties/age/type", instanceLocation: "/age",
# errors: {"type" => "\"not_an_integer\" is not of type \"integer\""}}
# ]}Hierarchical output — nested tree following schema structure:
evaluation.hierarchical
# => {valid: false, evaluationPath: "", schemaLocation: "", instanceLocation: "",
# details: [
# {valid: true, evaluationPath: "/type", schemaLocation: "/type", instanceLocation: ""},
# {valid: false, evaluationPath: "/required", schemaLocation: "/required",
# instanceLocation: "",
# errors: {"required" => "\"name\" is a required property"}},
# {valid: false, evaluationPath: "/properties", schemaLocation: "/properties",
# instanceLocation: "", droppedAnnotations: ["age"],
# details: [
# {valid: false, evaluationPath: "/properties/age",
# schemaLocation: "/properties/age", instanceLocation: "/age",
# details: [
# {valid: false, evaluationPath: "/properties/age/type",
# schemaLocation: "/properties/age/type", instanceLocation: "/age",
# errors: {"type" => "\"not_an_integer\" is not of type \"integer\""}}
# ]}
# ]}
# ]}Collected errors — flat list of all errors across evaluation nodes:
evaluation.errors
# => [{schemaLocation: "/required", absoluteKeywordLocation: nil,
# instanceLocation: "", error: "\"name\" is a required property"},
# {schemaLocation: "/properties/age/type", absoluteKeywordLocation: nil,
# instanceLocation: "/age",
# error: "\"not_an_integer\" is not of type \"integer\""}]Collected annotations — flat list of annotations from successfully validated nodes.
When a node fails validation, its annotations appear as droppedAnnotations in the list/hierarchical output instead.
valid_eval = validator.evaluate({ "name" => "Alice", "age" => 30 })
valid_eval.annotations
# => [{schemaLocation: "/properties", absoluteKeywordLocation: nil,
# instanceLocation: "", annotations: ["age", "name"]}]Use Canonical::JSON.to_string when you need a stable JSON representation:
schema_a = { "type" => "object", "properties" => { "b" => { "type" => "integer" }, "a" => { "type" => "string" } } }
schema_b = { "properties" => { "a" => { "type" => "string" }, "b" => { "type" => "integer" } }, "type" => "object" }
dump_a = JSONSchema::Canonical::JSON.to_string(schema_a)
dump_b = JSONSchema::Canonical::JSON.to_string(schema_b)
dump_a == dump_b # => trueMain use case: deduplicating equivalent JSON Schemas.
Produce a Compound Schema Document (Appendix B) by embedding all external $ref targets into a draft-appropriate container. The result validates identically to the original.
address_schema = {
"$schema" => "https://json-schema.org/draft/2020-12/schema",
"$id" => "https://example.com/address.json",
"type" => "object",
"properties" => { "street" => { "type" => "string" }, "city" => { "type" => "string" } },
"required" => ["street", "city"]
}
schema = {
"$schema" => "https://json-schema.org/draft/2020-12/schema",
"type" => "object",
"properties" => { "home" => { "$ref" => "https://example.com/address.json" } },
"required" => ["home"]
}
registry = JSONSchema::Registry.new([["https://example.com/address.json", address_schema]])
bundled = JSONSchema.bundle(schema, registry: registry)Validate that a JSON Schema document is itself valid:
JSONSchema::Meta.valid?({ "type" => "string" }) # => true
JSONSchema::Meta.valid?({ "type" => "invalid_type" }) # => false
begin
JSONSchema::Meta.validate!({ "type" => 123 })
rescue JSONSchema::ValidationError => e
e.message # => "123 is not valid under any of the schemas listed in the 'anyOf' keyword"
endBy default, jsonschema resolves HTTP references and file references from the local file system. You can implement a custom retriever to handle external references:
schemas = {
"https://example.com/person.json" => {
"type" => "object",
"properties" => {
"name" => { "type" => "string" },
"age" => { "type" => "integer" }
},
"required" => ["name", "age"]
}
}
retriever = ->(uri) { schemas[uri] }
schema = { "$ref" => "https://example.com/person.json" }
validator = JSONSchema.validator_for(schema, retriever: retriever)
validator.valid?({ "name" => "Alice", "age" => 30 }) # => true
validator.valid?({ "name" => "Bob" }) # => false (missing "age")For applications that frequently use the same schemas, create a registry to store and reference them:
registry = JSONSchema::Registry.new([
["https://example.com/address.json", {
"type" => "object",
"properties" => {
"street" => { "type" => "string" },
"city" => { "type" => "string" }
}
}],
["https://example.com/person.json", {
"type" => "object",
"properties" => {
"name" => { "type" => "string" },
"address" => { "$ref" => "https://example.com/address.json" }
}
}]
])
validator = JSONSchema.validator_for(
{ "$ref" => "https://example.com/person.json" },
registry: registry
)
validator.valid?({
"name" => "John",
"address" => { "street" => "Main St", "city" => "Boston" }
}) # => trueThe registry also accepts draft: and retriever: options:
registry = JSONSchema::Registry.new(
[["https://example.com/person.json", schemas["https://example.com/person.json"]]],
draft: :draft7,
retriever: retriever
)When validating schemas with regex patterns (in pattern or patternProperties), you can configure the underlying regex engine:
# Default fancy-regex engine with backtracking limits
# (supports lookaround and backreferences but needs protection against DoS)
validator = JSONSchema.validator_for(
{ "type" => "string", "pattern" => "^(a+)+$" },
pattern_options: JSONSchema::FancyRegexOptions.new(backtrack_limit: 10_000)
)
# Standard regex engine for guaranteed linear-time matching
# (prevents regex DoS attacks but supports fewer features)
validator = JSONSchema.validator_for(
{ "type" => "string", "pattern" => "^a+$" },
pattern_options: JSONSchema::RegexOptions.new
)
# Both engines support memory usage configuration
validator = JSONSchema.validator_for(
{ "type" => "string", "pattern" => "^a+$" },
pattern_options: JSONSchema::RegexOptions.new(
size_limit: 1024 * 1024, # Maximum compiled pattern size
dfa_size_limit: 10240 # Maximum DFA cache size
)
)The available options:
-
FancyRegexOptions: Default engine with lookaround and backreferences supportbacktrack_limit: Maximum backtracking stepssize_limit: Maximum compiled regex size in bytesdfa_size_limit: Maximum DFA cache size in bytes
-
RegexOptions: Safer engine with linear-time guaranteesize_limit: Maximum compiled regex size in bytesdfa_size_limit: Maximum DFA cache size in bytes
This configuration is crucial when working with untrusted schemas where attackers might craft malicious regex patterns.
When validating email addresses using {"format": "email"}, you can customize the validation behavior:
# Require a top-level domain (reject "user@localhost")
validator = JSONSchema.validator_for(
{ "format" => "email", "type" => "string" },
validate_formats: true,
email_options: JSONSchema::EmailOptions.new(require_tld: true)
)
validator.valid?("user@localhost") # => false
validator.valid?("[email protected]") # => true
# Disallow IP address literals and display names
validator = JSONSchema.validator_for(
{ "format" => "email", "type" => "string" },
validate_formats: true,
email_options: JSONSchema::EmailOptions.new(
allow_domain_literal: false, # Reject "user@[127.0.0.1]"
allow_display_text: false # Reject "Name <[email protected]>"
)
)
# Require minimum domain segments
validator = JSONSchema.validator_for(
{ "format" => "email", "type" => "string" },
validate_formats: true,
email_options: JSONSchema::EmailOptions.new(minimum_sub_domains: 3) # e.g., [email protected]
)Available options:
require_tld: Require a top-level domain (e.g., reject "user@localhost")allow_domain_literal: Allow IP address literals like "user@[127.0.0.1]" (default: true)allow_display_text: Allow display names like "Name [email protected]" (default: true)minimum_sub_domains: Minimum number of domain segments required
jsonschema provides detailed validation errors through the ValidationError class:
schema = { "type" => "string", "maxLength" => 5 }
begin
JSONSchema.validate!(schema, "too long")
rescue JSONSchema::ValidationError => error
# Basic error information
error.message # => '"too long" is longer than 5 characters'
error.verbose_message # => Full context with schema path and instance
error.instance_path # => Location in the instance that failed
error.schema_path # => Location in the schema that failed
# Detailed error information via `kind`
error.kind.name # => "maxLength"
error.kind.value # => { "limit" => 5 }
error.kind.to_h # => { "name" => "maxLength", "value" => { "limit" => 5 } }
endEach error has a kind property with convenient accessors:
JSONSchema.each_error({ "minimum" => 5 }, 3).each do |error|
error.kind.name # => "minimum"
error.kind.value # => { "limit" => 5 }
error.kind.to_h # => { "name" => "minimum", "value" => { "limit" => 5 } }
error.kind.to_s # => "minimum"
endWhen working with sensitive data, you can mask instance values in error messages:
schema = {
"type" => "object",
"properties" => {
"password" => { "type" => "string", "minLength" => 8 },
"api_key" => { "type" => "string", "pattern" => "^[A-Z0-9]{32}$" }
}
}
validator = JSONSchema.validator_for(schema, mask: "[REDACTED]")
begin
validator.validate!({ "password" => "123", "api_key" => "secret_key_123" })
rescue JSONSchema::ValidationError => exc
puts exc.message
# => '[REDACTED] does not match "^[A-Z0-9]{32}$"'
puts exc.verbose_message
# => '[REDACTED] does not match "^[A-Z0-9]{32}$"\n\nFailed validating...\nOn instance["api_key"]:\n [REDACTED]'
endJSONSchema::ValidationError- raised on validation failuremessage,verbose_message,instance_path,schema_path,evaluation_path,kind,instance- JSON Pointer helpers:
instance_path_pointer,schema_path_pointer,evaluation_path_pointer
JSONSchema::ReferencingError- raised when$refcannot be resolved
One-off validation methods (valid?, validate!, each_error, evaluate) accept these keyword arguments:
JSONSchema.valid?(schema, instance,
draft: :draft7, # Specific draft version (symbol)
validate_formats: true, # Enable format validation (default: false)
ignore_unknown_formats: true, # Don't error on unknown formats (default: true)
base_uri: "https://example.com", # Base URI for reference resolution
mask: "[REDACTED]", # Mask sensitive data in error messages
retriever: ->(uri) { ... }, # Custom schema retriever for $ref
formats: { "name" => proc }, # Custom format validators
keywords: { "name" => Klass }, # Custom keyword validators
registry: registry, # Pre-registered schemas
pattern_options: opts, # RegexOptions or FancyRegexOptions
email_options: opts, # EmailOptions
http_options: opts # HttpOptions
)evaluate accepts the same options except mask (currently unsupported for evaluation output).
validator_for accepts the same options except draft: — use draft-specific validators (Draft7Validator.new, etc.) to pin a draft version.
Valid draft symbols: :draft4, :draft6, :draft7, :draft201909, :draft202012.
jsonschema is designed for high performance, outperforming other Ruby JSON Schema validators in most scenarios:
- 28-148x faster than
json_schemerfor complex schemas and large instances - 200-567x faster than
json-schemawhere supported - 7-130x faster than
rj_schema(RapidJSON/C++)
For detailed benchmarks, see our full performance comparison.
This library draws API design inspiration from the Python jsonschema package. We're grateful to the Python jsonschema maintainers and contributors for their pioneering work in JSON Schema validation.
If you have questions, need help, or want to suggest improvements, please use GitHub Discussions.
If you find jsonschema useful, please consider sponsoring its development.
See CONTRIBUTING.md for details.
Licensed under MIT License.