Benchmarks
json-tology is not yet uniformly faster than its comparators. Where we win, where we lose, and where we have work to do.
This page is the honest read on json-tology's performance against four comparators: AJV, Zod, TypeBox (interpreted Value and compiled TypeCompiler), and Valibot. The numbers below come from the bench suite under bench/. Run npm run bench:report to regenerate.
The headline: today, json-tology loses most warm validation scenarios to TypeBox compiled, and loses some warm scenarios to AJV / Valibot. It wins at structural diff, transform encode, schema authoring (extend build), and validating invalid simple data with full error collection. Most of the loss surface comes from the canonical-graph-first execution model: every validate goes through the graph, which buys downstream features (ABox projection, OWL/SHACL emission, semantic round-trip) but pays a per-call cost a code-gen-only validator does not.
We're publishing the full picture rather than cherry-picking. Where we are slow, we say so.
How to read this page
WIN— json-tology is at least 1.05x faster than the comparator on the listed scenario.LOSS— json-tology is at least 1.05x slower.EVEN— within 5% either way.ops/s— operations per second after warmup. Higher is better.ns/op— nanoseconds per operation. Lower is better.json-tology vs this— the multiplier between json-tology and the listed library. "2.39x faster" means json-tology runs 2.39 ops for every 1 op of the comparator on this scenario.
The latest run is auto-generated from bench/results/latest.md and is included in full at the end of this page.
What's in scope
The bench suite covers the operations json-tology actually exposes:
| Suite | json-tology surface | Comparators |
|---|---|---|
| Validation | registry.validate | AJV validate, TypeBox TypeCompiler.Check, Zod safeParse, Valibot safeParse |
| Instantiation | registry.instantiate (no coercion) | TypeBox Value.Parse, Zod parse, Valibot parse |
| Coerce | registry.instantiate with castTypes: true, defaults | TypeBox Value.Parse, Zod parse, Valibot parse |
| Value operations | Value.clone, Value.diff, registry.clean, registry.convert | TypeBox Value.Clean / Convert / Diff, structuredClone |
| Transforms | Transform.create decode + facade encode | TypeBox Value.Decode / Value.Encode, Zod .transform |
| Composition | Compose.extend / intersection / discriminatedUnion | TypeBox Type.Composite / Intersect / Union, Zod .extend / intersection / discriminatedUnion, Valibot variant |
| Serialization | dump, dumpJson, facade encode | JSON.stringify, structuredClone, TypeBox Value.Encode |
| Registry | cold register + first validate, warm validate | TypeBox TypeCompiler.Compile + Check, Zod, Valibot |
| Compiled vs Interpreted | SchemaCompiler vs GraphEngine.execute | internal — measures the speedup of the compile path |
What's unique
These are operations no comparator implements, so they appear only as a single-library row in the report. They are included for completeness, not as head-to-head wins.
toTbox— OWL TBox projection from the canonical graph.toShacl— SHACL shape projection.toQuads/fromQuads— RDF round-trip via projection.- ABox projection through
Materializer.projectAbox. findDuplicatesover the registry.- The
jt:keyword set (computed properties, invariants, decoders, brands). - OWL / SHACL emission through
OntologyBuilder.
These are surface area that competitors do not expose. They are not wins; they are work the competitors do not do.
Where we have work to do
Pulled directly from bench/results/latest.md. These are scenarios where json-tology is more than 5x slower than the median comparator. Each is a known issue.
simple validvalidation (~6x slower than median) — the per-validate graph traversal cost dominates a 5-property flat schema. Tracked: rework hot path so flat schemas skip subgraph dispatch when no $refs are present.nested validvalidation (~7x slower than median) — same root cause assimple valid, amplified by per-property subschema lookup. Tracked: precompile a flattened property dispatch table per schema graph.convert simple(~32x slower than TypeBox) —castTypes: trueruns a separate normalize pass over the value before validate. Tracked: fold normalize into the compiled validator.extend + validatecold path (~12x slower than TypeBox) — registering a derived schema rebuilds the canonical graph from scratch per call. Tracked: cache subgraph fragments at register time.intersectioncold path (~12x slower than Zod) — same root cause asextend + validate.dumpJson nested(~8x slower thanJSON.stringify) —dumpwalks the schema graph for every property. Tracked: short-circuit when no Transform encoders are attached anywhere in the subgraph.discriminated unionwarm (~74x slower than TypeBox compiled) — every variant is currently re-resolved throughoneOfsemantics. Tracked: discriminator-aware fast path that switches directly on the discriminator key.cold first validate(~156x slower than Valibot) — Valibot has no compile step at all; the cold path is the warm path. Our cold path includes graph construction, subschema linking, and JIT compilation. Acceptable cost because subsequent calls are fast, but the gap is honest. Tracked: lazy subgraph build for unreachable parts of the schema.
If we are slower on a scenario by more than 5x, you can expect a corresponding tracked item above. None of these are blockers for using the library — but they are real, and we are working on them.
Reproduce
npm install
npm run bench:reportThe runner prints to console and writes bench/results/latest.md. Bench numbers move with hardware. The values embedded below came from the developer machine listed in the Environment section. CI runs are uploaded as workflow artifacts (see .github/workflows/bench.yml); use those as the canonical reference once bench-in-CI is established.
For deeper investigation:
npm run bench # human-readable console output, no markdown
npm run bench:flame # 0x flame graph profiling under .flame/Latest run
The block below is auto-generated. To refresh it, run npm run bench:report.
json-tology benchmarks — latest run
Generated by npm run bench:report from bench/run.ts. Numbers move with hardware; treat as a directional snapshot, not absolutes.
Environment
- Date: 2026-05-06T01:04:02.503Z
- Node: v24.13.0
- OS: darwin 25.3.0
- CPU: Apple M4 Max
Scorecard
| Comparator | Wins | Ties | Losses | Win rate |
|---|---|---|---|---|
| typebox | 6 | 0 | 15 | 29% |
| ajv | 0 | 0 | 3 | 0% |
| zod | 2 | 0 | 11 | 15% |
| valibot | 0 | 0 | 9 | 0% |
| structuredClone | 1 | 0 | 1 | 50% |
| JSON.stringify | 0 | 0 | 1 | 0% |
Validation
simple valid
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 435,362 | 2297 | - | - |
| typebox | 22,388,063 | 45 | 51.42x slower | LOSS |
| ajv | 1,596,029 | 627 | 3.67x slower | LOSS |
| zod | 550,706 | 1816 | 1.26x slower | LOSS |
| valibot | 1,254,756 | 797 | 2.88x slower | LOSS |
simple invalid
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 240,134 | 4164 | - | - |
| typebox | 293,324 | 3409 | 1.22x slower | LOSS |
| ajv | 9,980,953 | 100 | 41.56x slower | LOSS |
| zod | 26,308 | 38011 | 9.13x faster | WIN |
| valibot | 527,777 | 1895 | 2.20x slower | LOSS |
nested valid
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 144,609 | 6915 | - | - |
| typebox | 5,565,695 | 180 | 38.49x slower | LOSS |
| ajv | 2,817,986 | 355 | 19.49x slower | LOSS |
| zod | 197,376 | 5066 | 1.36x slower | LOSS |
| valibot | 307,725 | 3250 | 2.13x slower | LOSS |
Instantiation
instantiate simple
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 240,652 | 4155 | - | - |
| typebox | 415,657 | 2406 | 1.73x slower | LOSS |
| zod | 622,687 | 1606 | 2.59x slower | LOSS |
| valibot | 1,235,263 | 810 | 5.13x slower | LOSS |
instantiate nested
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 90,976 | 10992 | - | - |
| typebox | 62,495 | 16001 | 1.46x faster | WIN |
| zod | 365,133 | 2739 | 4.01x slower | LOSS |
| valibot | 275,905 | 3624 | 3.03x slower | LOSS |
Coerce
coerce valid
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 336,806 | 2969 | - | - |
| typebox | 217,645 | 4595 | 1.55x faster | WIN |
| zod | 3,342,069 | 299 | 9.92x slower | LOSS |
| valibot | 2,628,671 | 380 | 7.80x slower | LOSS |
coerce defaults
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 162,931 | 6138 | - | - |
| typebox | 490,139 | 2040 | 3.01x slower | LOSS |
Value operations
clean simple
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 174,304 | 5737 | - | - |
| typebox | 407,690 | 2453 | 2.34x slower | LOSS |
clean nested
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 58,013 | 17237 | - | - |
| typebox | 103,761 | 9638 | 1.79x slower | LOSS |
convert simple
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 197,050 | 5075 | - | - |
| typebox | 1,889,460 | 529 | 9.59x slower | LOSS |
clone nested
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 217,000 | 4608 | - | - |
| structuredClone | 150,703 | 6636 | 1.44x faster | WIN |
diff nested
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 414,935 | 2410 | - | - |
| typebox | 54,124 | 18476 | 7.67x faster | WIN |
Transforms
decode date
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 269,301 | 3713 | - | - |
| typebox | 3,168,267 | 316 | 11.76x slower | LOSS |
| zod | 741,597 | 1348 | 2.75x slower | LOSS |
encode date
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 1,870,937 | 534 | - | - |
| typebox | 409,693 | 2441 | 4.57x faster | WIN |
Composition
extend build
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 1,747,352 | 572 | - | - |
| typebox | 705,304 | 1418 | 2.48x faster | WIN |
| zod | 12,702 | 78729 | 137.57x faster | WIN |
extend + validate
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 8,773 | 113987 | - | - |
| typebox | 81,403 | 12285 | 9.28x slower | LOSS |
| zod | 9,446 | 105867 | 1.08x slower | LOSS |
discriminated union
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 396,412 | 2523 | - | - |
| typebox | 50,588,087 | 20 | 127.61x slower | LOSS |
| zod | 1,772,771 | 564 | 4.47x slower | LOSS |
| valibot | 1,634,634 | 612 | 4.12x slower | LOSS |
intersection
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 9,907 | 100940 | - | - |
| typebox | 59,743 | 16738 | 6.03x slower | LOSS |
| zod | 65,392 | 15292 | 6.60x slower | LOSS |
Serialization
dump nested
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 46,865 | 21338 | - | - |
| structuredClone | 185,011 | 5405 | 3.95x slower | LOSS |
| typebox | 105,605 | 9469 | 2.25x slower | LOSS |
dumpJson nested
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 44,889 | 22277 | - | - |
| JSON.stringify | 853,520 | 1172 | 19.01x slower | LOSS |
encode event
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 2,275,822 | 439 | - | - |
| typebox | 955,105 | 1047 | 2.38x faster | WIN |
Registry
cold first validate
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 2,311 | 432654 | - | - |
| typebox | 19,703 | 50752 | 8.53x slower | LOSS |
| zod | 6,271 | 159471 | 2.71x slower | LOSS |
| valibot | 295,260 | 3387 | 127.76x slower | LOSS |
warm validate
| Library | ops/s | ns/op | json-tology vs this | Status |
|---|---|---|---|---|
| json-tology | 117,723 | 8495 | - | - |
| typebox | 730,478 | 1369 | 6.21x slower | LOSS |
| zod | 1,208,842 | 827 | 10.27x slower | LOSS |
| valibot | 292,180 | 3423 | 2.48x slower | LOSS |
Compiled vs Interpreted
compiled simple valid
Unique to json-tology — no head-to-head comparator.
| Library | ops/s | ns/op |
|---|---|---|
| compiled | 824,433 | 1213 |
| interpreted | 33,288 | 30041 |
compiled simple invalid
Unique to json-tology — no head-to-head comparator.
| Library | ops/s | ns/op |
|---|---|---|
| compiled | 509,928 | 1961 |
| interpreted | 34,536 | 28955 |
compiled nested valid
Unique to json-tology — no head-to-head comparator.
| Library | ops/s | ns/op |
|---|---|---|
| compiled | 183,375 | 5453 |
| interpreted | 5,785 | 172859 |
Where we have work to do
Scenarios where json-tology is more than 5x slower than the median comparator:
nested valid: 19.49x slower than median comparatorcoerce valid: 7.80x slower than median comparatorconvert simple: 9.59x slower than median comparatordecode date: 11.76x slower than median comparatorextend + validate: 9.28x slower than median comparatorintersection: 6.60x slower than median comparatordumpJson nested: 19.01x slower than median comparatorcold first validate: 8.53x slower than median comparatorwarm validate: 6.21x slower than median comparator
Reproduce
npm install
npm run bench:reportBench numbers move with hardware. The values committed here came from the developer machine listed in Environment. CI runs are uploaded as workflow artifacts (see .github/workflows/bench.yml).