ExJsonschema

View Source

🚀 High-performance JSON Schema validation for Elixir using Rust

ExJsonschema is a fast, safe, and spec-compliant JSON Schema validator for Elixir, powered by the battle-tested Rust jsonschema crate. It provides an idiomatic Elixir API with detailed error reporting and supports multiple JSON Schema draft versions.

✨ Features

  • 🔥 High Performance: Rust-powered validation with zero-copy JSON processing
  • 🛡️ Memory Safe: No risk of crashing the BEAM VM - all Rust panics are caught
  • 📋 Spec Compliant: Supports JSON Schema draft-07, draft 2019-09, and draft 2020-12
  • 🔍 Detailed Errors: Rich error messages with path information and validation context
  • 📦 Zero Dependencies: Precompiled NIFs mean no Rust toolchain required for end users
  • 🎯 Idiomatic Elixir: Clean, functional API that feels natural in Elixir

📋 Installation

Add ex_jsonschema to your list of dependencies in mix.exs:

def deps do
  [
    {:ex_jsonschema, "~> 0.1.0"}
  ]
end

That's it! No Rust toolchain required - precompiled binaries are included.

🚀 Quick Start

# Compile a schema once
schema = ~s({
  "type": "object",
  "properties": {
    "name": {"type": "string", "minLength": 1},
    "age": {"type": "number", "minimum": 0}
  },
  "required": ["name"]
})

{:ok, compiled} = ExJsonschema.compile(schema)

# Validate JSON - fast validation
valid_json = ~s({"name": "Alice", "age": 30})
:ok = ExJsonschema.validate(compiled, valid_json)

# Invalid JSON with detailed errors
invalid_json = ~s({"age": -5})
{:error, errors} = ExJsonschema.validate(compiled, invalid_json)

Enum.each(errors, fn error ->
  IO.puts("❌ #{error.message} at #{error.instance_path}")
end)
# Output:
# ❌ "name" is a required property at 
# ❌ -5 is less than the minimum of 0 at /age

📖 API Reference

Schema Compilation

# Compile a schema
{:ok, compiled} = ExJsonschema.compile(schema_json)

# Compile with error handling
case ExJsonschema.compile(schema_json) do
  {:ok, compiled} -> 
    # Use compiled schema
  {:error, %ExJsonschema.CompilationError{} = error} ->
    IO.puts("Schema error: #{error}")
end

# Compile and raise on error
compiled = ExJsonschema.compile!(schema_json)

Validation

# Full validation with detailed errors
case ExJsonschema.validate(compiled, json) do
  :ok -> 
    IO.puts("Valid!")
  {:error, errors} ->
    Enum.each(errors, &IO.puts("Error: #{&1}"))
end

# Quick validity check (faster)
if ExJsonschema.valid?(compiled, json) do
  IO.puts("Valid!")
end

# One-shot validation (compile + validate)
ExJsonschema.validate_once(schema_json, instance_json)

# Validation with exceptions
ExJsonschema.validate!(compiled, json)

🔍 Enhanced Error Handling

ExJsonschema provides detailed error information to help you debug validation issues:

Schema Compilation Errors

# JSON parsing error
invalid_json = ~s({"type": "string)  # Missing quote
{:error, error} = ExJsonschema.compile(invalid_json)

error.type     # :json_parse_error
error.message  # "Invalid JSON: EOF while parsing a string at line 1 column 16"
error.details  # "Failed to parse JSON at line 1, column 16"

# Schema validation error  
invalid_schema = ~s({"type": "invalid_type"})
{:error, error} = ExJsonschema.compile(invalid_schema)

error.type     # :compilation_error  
error.message  # "Schema compilation failed"
error.details  # "\"invalid_type\" is not valid under any of the schemas listed in the 'anyOf' keyword"

Validation Errors

schema = ~s({
  "type": "object",
  "properties": {
    "user": {
      "type": "object", 
      "properties": {
        "age": {"type": "number", "minimum": 0}
      }
    }
  }
})

{:ok, compiled} = ExJsonschema.compile(schema)
{:error, [error]} = ExJsonschema.validate(compiled, ~s({"user": {"age": -5}}))

error.instance_path  # "/user/age"
error.schema_path    # "/properties/user/properties/age/minimum"  
error.message        # "-5 is less than the minimum of 0"

🏗️ Building from Source

For development or if you need to build from source:

# Clone the repository
git clone https://github.com/your-username/ex_jsonschema.git
cd ex_jsonschema

# Install dependencies
mix deps.get

# Force build from source (requires Rust toolchain)
EX_JSONSCHEMA_BUILD=1 mix compile

# Run tests
EX_JSONSCHEMA_BUILD=1 mix test

🎯 Performance

ExJsonschema is designed for high-performance applications:

  • Compiled schemas: Compile once, validate many times
  • Zero-copy: Direct validation of JSON strings without intermediate parsing
  • Rust performance: Orders of magnitude faster than pure Elixir implementations
  • Memory efficient: Minimal memory allocation during validation

🤝 JSON Schema Support

ExJsonschema supports multiple JSON Schema draft versions:

  • Draft 7 (2019)
  • Draft 2019-09
  • Draft 2020-12 (latest)

All core keywords and validation rules are supported, including:

  • Type validation (string, number, object, array, etc.)
  • Constraints (minimum, maximum, minLength, maxLength, etc.)
  • Object validation (properties, required, additionalProperties, etc.)
  • Array validation (items, minItems, maxItems, etc.)
  • String formats (email, uri, date-time, etc.)
  • Conditional schemas (if/then/else, allOf, anyOf, oneOf)

📚 Examples

User Registration Validation

user_schema = ~s({
  "type": "object",
  "properties": {
    "email": {"type": "string", "format": "email"},
    "username": {"type": "string", "minLength": 3, "maxLength": 20},
    "age": {"type": "integer", "minimum": 13, "maximum": 120},
    "preferences": {
      "type": "object",
      "properties": {
        "newsletter": {"type": "boolean"},
        "theme": {"type": "string", "enum": ["light", "dark"]}
      }
    }
  },
  "required": ["email", "username", "age"]
})

{:ok, compiled} = ExJsonschema.compile(user_schema)

# Valid user
user_data = ~s({
  "email": "alice@example.com",
  "username": "alice_cooper", 
  "age": 25,
  "preferences": {
    "newsletter": true,
    "theme": "dark"
  }
})

:ok = ExJsonschema.validate(compiled, user_data)

API Response Validation

api_response_schema = ~s({
  "type": "object",
  "properties": {
    "status": {"type": "string", "enum": ["success", "error"]},
    "data": {"type": "object"},
    "message": {"type": "string"},
    "timestamp": {"type": "string", "format": "date-time"}
  },
  "required": ["status", "timestamp"],
  "if": {"properties": {"status": {"const": "error"}}},
  "then": {"required": ["message"]},
  "else": {"required": ["data"]}
})

🔧 Development

Requirements

  • Elixir 1.12+
  • Erlang/OTP 22+
  • Rust 1.70+ (only for building from source)

Running Tests

mix test                    # Uses precompiled NIF
EX_JSONSCHEMA_BUILD=1 mix test  # Builds from source

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🙏 Acknowledgments

  • Built on the excellent jsonschema Rust crate
  • Powered by Rustler for safe Rust-Elixir interop
  • Inspired by the JSON Schema specification and community