Warning
This entire project — code, tests, optimizations, benchmarks, and this README — was autonomously written by Claude Code. No human reviewed the benchmark methodology or verified these numbers are fair. The AI may have unknowingly introduced shortcuts that inflate performance, or configured benchmarks in ways that favor this implementation. We found and fixed 17 correctness bugs during audits, but more may exist. Treat these benchmarks as rough, unverified estimates — not rigorous measurements. If you find anything wrong, please open an issue — it will be fixed immediately.
A JSON Schema validator for Zig — 100% spec-compliant and built for performance.
Warm mode — schema pre-compiled, 550 instances × 100 iterations (methodology):
| Dataset | jsonschema.zig | jsonschema (Rust) | jsonschema (Go) | Ajv (JS) | jsonschema (Python) |
|---|---|---|---|---|---|
| helm-chart-lock | 8 ms | 13 ms | 293 ms | 53 ms | 2,348 ms |
| dependabot | 19 ms | 27 ms | 262 ms | 26 ms | 2,500 ms |
| geojson | 46 ms | 53 ms | 2,035 ms | 1,262 ms | 28,490 ms |
| openapi | 223 ms | 7,454 ms | 4,219 ms | 262 ms | 345,985 ms |
| tsconfig | 25 ms | 89 ms | 367 ms | — | 4,475 ms |
| github-workflow | 59 ms | 56 ms | 1,259 ms | 300 ms | 21,693 ms |
| package-json | 56 ms | — | — | — | 3,163 ms |
| cspell | 24 ms | — | — | 244 ms | 5,468 ms |
Cold mode — per-instance median of schema compilation + single validation (550 cycles, Docker-isolated):
| Dataset | jsonschema.zig | jsonschema (Rust) | jsonschema (Go) | Ajv (JS) | jsonschema (Python) |
|---|---|---|---|---|---|
| helm-chart-lock | 14 us | 14 us | 156 us | 302 us | 46 us |
| dependabot | 38 us | 84 us | 379 us | 885 us | 53 us |
| geojson | 184 us | 545 us | 2,796 us | 13,796 us | 379 us |
| openapi | 279 us | 22,841 us | 2,940 us | 7,225 us | 5,511 us |
| tsconfig | 292 us | 3,909 us | 3,376 us | — | 87 us |
| github-workflow | 227 us | 947 us | 3,261 us | 13,269 us | 384 us |
| package-json | 209 us | 335 us | 1,898 us | — | 65 us |
| cspell | 247 us | 432 us | 2,652 us | 8,530 us | 105 us |
Benchmark details
- Machine: Apple M4 Pro (12 cores, 48GB RAM), macOS 15.5
- Isolation: Docker containers per language
- Method: Warm: median of 5 runs, 100 iterations; Cold: per-instance median of 550 compile+validate cycles; boolean-only validation (
is_valid), format validation disabled - Schemas: Real-world schemas from SchemaStore, OpenAPI Initiative, GeoJSON
- Instances: Synthetically generated (500 valid + 50 invalid per dataset, deterministic seed)
- "—" = the library failed to compile or validate the schema
- Reproduction: github.com/ktmage/jsonschema-bench
- Full specification coverage: Draft 7 (920/920 tests) and Draft 2020-12 (1275/1275 tests) with 100% pass rate against the official JSON Schema Test Suite
- ECMA-262 regex support: QuickJS libregexp — full ECMA-262 compliance including lookahead, backreferences, and Unicode
- Compiled schema mode: Pre-compile schemas once, validate many times with pre-linked sub-schema dispatch and zero-allocation fast paths
- Detailed error reporting: JSON Pointer paths to both the failing instance location and the schema keyword that rejected it
const std = @import("std");
const jsonschema = @import("jsonschema");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const schema_json =
\\{
\\ "type": "object",
\\ "properties": {
\\ "name": { "type": "string" },
\\ "age": { "type": "integer", "minimum": 0 }
\\ },
\\ "required": ["name"]
\\}
;
const instance_json =
\\{ "name": "Alice", "age": 30 }
;
const schema = try std.json.parseFromSlice(std.json.Value, allocator, schema_json, .{});
defer schema.deinit();
const instance = try std.json.parseFromSlice(std.json.Value, allocator, instance_json, .{});
defer instance.deinit();
const result = jsonschema.validate(allocator, schema.value, instance.value);
defer result.deinit();
if (result.isValid()) {
std.debug.print("Valid!\n", .{});
} else {
for (result.errors) |err| {
std.debug.print("{s}: {s}\n", .{ err.instance_path, err.message });
}
}
}When validating many instances against the same schema, compile it once for significantly better throughput:
// Compile once
var compiled = jsonschema.CompiledSchema.compile(allocator, schema.value, null);
defer compiled.deinit();
// Validate many times
for (instances) |instance| {
const result = jsonschema.validateCompiled(allocator, &compiled, instance);
defer result.deinit();
// ...
}The compiled path pre-links sub-schema references, eliminates hash-map lookups, and enables a zero-allocation isValidFast path for common schema patterns.
Memory model:
CompiledSchemaholds internal references to the original parsedschemaJSON value. The parsed schema must outlive theCompiledSchema— do not callschema.deinit()beforecompiled.deinit(). TheCompiledSchemaowns an internal arena allocator that is freed ondeinit().Thread safety: A
CompiledSchemais read-only after compilation and can be shared across threads for concurrent validation. Each validation call must use its own allocator andValidationResult. The original schema JSON must not be modified or freed while any thread is validating.
Add to your build.zig.zon:
.dependencies = .{
.jsonschema = .{
.url = "https://github.com/ktmage/jsonschema.zig/archive/refs/tags/v0.1.0.tar.gz",
.hash = "HASH", // run: zig fetch <url>
},
},Then in your build.zig:
const jsonschema_dep = b.dependency("jsonschema", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("jsonschema", jsonschema_dep.module("jsonschema"));Tested against the official JSON Schema Test Suite:
- Draft 7: 920/920 (100%)
- Draft 2020-12: 1142/1142 (100%)
Draft 7: type, enum, const, minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, minLength, maxLength, pattern, items, additionalItems, minItems, maxItems, uniqueItems, contains, properties, required, additionalProperties, patternProperties, minProperties, maxProperties, propertyNames, dependencies, allOf, anyOf, oneOf, not, if/then/else, $ref, definitions
Draft 2020-12 (additional): prefixItems, $defs, $anchor, dependentRequired, dependentSchemas, minContains, maxContains, unevaluatedProperties, unevaluatedItems, $dynamicRef, $dynamicAnchor
For schemas that reference external resources (via $ref with absolute URIs), you can register them:
var registry = jsonschema.SchemaRegistry.init(allocator);
defer registry.deinit();
// Register external schemas by URI
try registry.addSchema("https://example.com/address.json", address_schema.value);
// Validate with registry
const result = jsonschema.validateWithRegistry(
allocator,
schema.value,
instance.value,
®istry,
);Remote schemas: The library does not automatically fetch remote
$refURIs. Fetch schemas yourself (e.g., viastd.http.Client) and register them withregistry.addSchema()before validation.
Validation errors include JSON Pointer paths for precise error location:
for (result.errors) |err| {
// err.instance_path — where in the instance the error occurred (e.g. "/address/zip")
// err.schema_path — which schema keyword rejected it (e.g. "/properties/address/properties/zip/pattern")
// err.keyword — the keyword name (e.g. "pattern")
// err.message — human-readable description
}zig build # Build the library
zig build test # Run the full test suite (auto-fetches JSON Schema Test Suite)Requirements: Zig 0.14.0+
This project follows Semantic Versioning:
- Major (1.0.0 → 2.0.0): Breaking API changes
- Minor (0.1.0 → 0.2.0): New features, backward compatible
- Patch (0.1.0 → 0.1.1): Bug fixes only
The library is currently pre-1.0. The public API (validate, validateCompiled, validateWithRegistry, validateCompiledWithRegistry, isValidCompiled, CompiledSchema, SchemaRegistry, ValidationResult, CustomKeyword) may change between minor versions. A 1.0.0 release will signal API stability.
See CONTRIBUTING.md for development setup, code style, and PR guidelines.
MIT