ata-validator-crystal

Version, currently 0.1.03 versions

github.com/GroophyLifefor/ata-validator-crystal

Crystal FFI bindings for ata-validator, the C++20 JSON Schema validator

2 stars
0 dependents
License: MIT

Nothing has been indexed for 0.1.0 yet. The tag is recorded, its shard.yml has not been read, so the manifest and dependency list below are empty because they are unknown rather than because they are absent.

Installation

# Add this to your shard.yml
dependencies:
  ata-validator-crystal:
    github: GroophyLifefor/ata-validator-crystal
    version: ~> 0.1.0

Then run:

shards install

shard.yml

No shard.yml has been indexed for 0.1.0. You can read it on the repository.

Dependencies

Unknown: the shard.yml for this version has not been read yet.

README

This README is the one indexed from the repository at its latest ref, not from the tag for this version.

ata-validator-crystal

Crystal bindings for ata-validator — a high-performance C++20 JSON Schema validator.

The C++ core uses simdjson and the RE2 regex engine; this shard only wraps its pure C API (ata_c.h). Struct layouts and function signatures match ata_c.h exactly.

Install

Add to shard.yml:

dependencies:
  ata-validator-crystal:
    github: groophylifefor/ata-validator-crystal
    version: ~> 0.1.0
shards install

Native library

Two parts are required:

  • ata.lib (or libata.a) — at link time, passed to crystal build via /LIBPATH (Windows/MSVC) or -L (Unix)
  • ata.dll (or libata.so) — at runtime, next to the executable, on PATH, or pointed to by ATA_VALIDATOR_LIB

To build the library into libata/ (the ata-validator source must be a sibling directory):

crystal run scripts/build_native.cr

This script compiles the shared library in ata-validator with CMake and copies libata/ata.dll + libata/ata.lib into the package root. (Default source path is ../../ata-validator; override with the ATA_VALIDATOR_SRC env var.)

Build

Windows (MSVC linker):

crystal build src/main.cr --link-flags "/LIBPATH:ata-validator-crystal/libata"

Linux/macOS:

crystal build src/main.cr --link-flags "-Lata-validator-crystal/libata -lata"

Usage

require "ata-validator-crystal"

schema = <<-JSON
{
  "type": "object",
  "properties": {
    "name": {"type": "string", "minLength": 1},
    "age":  {"type": "integer", "minimum": 0}
  },
  "required": ["name"]
}
JSON

puts "ata v#{AtaValidator.version}"

# Reuse a compiled schema
validator = AtaValidator::Validator.new(schema)

result = validator.validate(%({"name": "Mert", "age": 28}))
puts result.valid                          # => true

result = validator.validate(%({"age": -1}))
puts result.valid                          # => false
result.errors.each do |e|
  puts "#{e.path}: #{e.message}"           # => /age: value -1.000000 < minimum 0.000000
end

validator.valid?(%({"name": "Mert"}))      # => true (quick boolean check)
validator.close

# One-shot (compiles the schema on every call)
AtaValidator.validate(schema, %({"name": "Mert"})).valid

Schema DSL

Ata.object is a compile-time macro: it reads the block's AST and generates a real struct with class methods schema_json, validate, valid? and from_json, plus a typed getter for every field. Every field is required unless optional: true is passed.

Ata.object User do
  string :name, min: 3, max: 10
  int :age, gt: 0, lte: 120
  string :email, format: "email", optional: true
  bool :active
end

User.valid?(%({"name": "Mert", "age": 28, "active": true}))   # => true
User.valid?(%({"name": "Me", "age": 28, "active": true}))     # => false (minLength)
User.valid?(%({"name": "Mert", "age": 0, "active": true}))    # => false (exclusiveMinimum)

u = User.from_json(%({"name": "Mert", "age": 28, "active": true}))
u.name                # => "Mert"  (typed getter)
u.email               # => nil     (optional field)

puts User.schema_json
# {"type":"object","properties":{"name":{"type":"string","minLength":3,"maxLength":10},
#  "age":{"type":"integer","exclusiveMinimum":0,"maximum":120},...},"required":["name","age","active"]}
MethodArgumentsJSON Schema
stringmin / maxminLength / maxLength, pattern, format, values:enum{"type": "string", ...}
intgt / ltexclusiveMinimum / exclusiveMaximum, gte / lteminimum / maximum{"type": "integer", ...}
floatsame as int{"type": "number", ...}
bool{"type": "boolean"}
any{} (any value accepted)
arrayof: :string/:int/:float/:bool/:any or a nested Ata.object schema; min_items / max_items{"type": "array", "items": {...}}
objectof: a nested Ata.object schemaembeds the schema as-is

Nested schemas compose:

Ata.object Address do
  string :city, min: 1
  int :zip, gte: 0
end

Ata.object Person do
  string :name, min: 3
  object :address, of: Address
  array :tags, of: :string, min_items: 1
end

gt: / lt: emit the draft-06/07 boolean-independent form ("exclusiveMinimum": 0), which is what the native validator implements.

API

  • AtaValidator.version : String
  • AtaValidator::Validator.new(schema_json) — compiles the schema once
    • validate(json) : ValidationResult (valid + errors)
    • valid?(json) : Bool
    • close / finalize — frees the compiled schema
  • AtaValidator.validate(schema_json, json) : ValidationResult — one-shot
  • Ata.object Name do ... end — macro DSL (see above)
    • Name.valid?(json) : Bool, Name.validate(json) : ValidationResult, Name.schema_json : String, Name.from_json(json) : Name
  • Error types: CompileError (invalid schema), ValidationError (path, message)

Test

crystal run scripts/build_native.cr
crystal spec --link-flags "/LIBPATH:libata"

Benchmarks

bench/ compares ata-validator-crystal against JSON::Serializable and Athena::Validator on five scenarios: parse valid JSON, invalid JSON, nested objects, 100k bulk validation and per-operation allocation.

Results (2026-08-01, Windows 11 / MSVC, Crystal 1.15.0, --release --no-debug)

Higher is better for throughput, lower is better for allocation.

ScenarioMetricata-validator-crystalJSON::SerializableAthena::Validator× vs JSON::Serializable× vs Athena::Validator
Parse valid JSONops/s750 968449 970299 6401.67×2.51×
Parse valid JSONbytes/op328161 23225.5×38.5×
Invalid JSON (malformed)ops/s641 2871 1381 090563.5×588.3×
Invalid JSON (malformed)bytes/op1125 5045 50449.1×49.1×
Nested objectops/s338 270304 255190 0741.11×1.78×
Nested objectbytes/op321 0251 63232.0×51.0×
100.000 validationops/s844 921407 730274 9172.07×3.07×
100.000 validationbytes/op328161 23225.5×38.5×
Allocationbytes/op328161 23225.5×38.5×

Reading the ratios: for ops/s rows, = ata-validator-crystal is N× faster; for bytes/op rows, = ata-validator-crystal allocates N× less memory.

Note on "Invalid JSON": JSON::Serializable and Athena::Validator raise a JSON::ParseException on malformed JSON, so each iteration pays exception-handling cost (hence the ~1 000 ops/s and 5.5 kB/op). ata-validator-crystal returns valid=false with a structured error list instead of throwing, which is why it stays fast.

These numbers are machine- and schema-specific — rerun locally with crystal build bench/bench.cr --release --no-debug to reproduce.

# build with the shared fixtures (requires the dev dependency: shards install)
crystal build bench/bench.cr -o bin/bench.exe --release --no-debug --link-flags "/LIBPATH:libata"

# run everything (100k / 20k iterations per scenario)
$env:PATH = "$PWD\libata;$env:PATH"
.\bin\bench.exe

# run a subset, or override iterations
.\bin\bench.exe nested
.\bin\bench.exe 10000

Each scenario prints:

  • correctness — whether each target accepts/rejects each fixture
  • throughput — total ms and ops/s over N iterations
  • allocation — heap bytes per operation (GC.stats delta)

Adding a scenario

Drop a new file into bench/scenarios/ — it is auto-required. Register a row with Bench.register:

require "../framework"

Bench.register("My scenario", description: "...", order: 6) do |s|
  s.fixture("valid", %({"name": "Mert", "age": 28}))
  s.fixture("invalid", %({"age": -1}))

  s.target("my-tool") { |json| my_tool_valid?(json) }
end

Reuse the shared types (Person, PersonWithAddress, Bench::SCHEMA_*, Bench.person_workloads) from bench/support.cr, or define your own. The first fixture is used for the throughput/allocation runs.

License

MIT. The C++ core comes from ata-validator, MIT licensed (original copyright preserved in LICENSE).