TL;DR: I built JetValidator - a JSON Schema validator that compiles 19x faster than AJV and wins 72% of validation benchmarks. I didn't set out to beat anyone. I was just building a backend framework and needed validation. One thing led to another.
π¦ npm: @jetio/validator | @jetio/schema-builder
π Benchmarks: Full Comparison | Interactive
π GitHub: validator | schema-builder
π Docs: official-jetio.github.io
Bonus: If raw JSON Schema feels painful, I built @jetio/schema-builder - fluent, type-safe, with nice-to-haves like .elseIf() that the spec forgot.
I didn't set out to build a JSON Schema validator.
I was building a backend framework. That's it. I needed validation for incoming requests, so I thought, let me just add that real quick.
Famous last words.
The Beginning (I Didn't Know Better)
My first validator was... weird. It only accepted objects. It used this strange schema builder pattern I made up. I didn't know JSON Schema was a thing. I didn't know there were specs, drafts, test suites, or that validators like AJV even existed.
But it worked. And it was fast because I chose compilation from the start. String concatenation. Generate the validation function, run it. Simple.
Then Claude (yes, the AI) suggested I benchmark it against AJV.
I was winning.
I thought: wait, what?
So I kept building. I dove deeper. I discovered JSON Schema the actual specification. And that's when I learned that a validator doesn't just validate objects.
A boolean can be a schema. You can validate strings, arrays, numbers. There are drafts. There are keywords. There are rules upon rules upon rules.
My entire plan shattered.
The Rewrite (And the One After That)
I calmed down. Took it easy. Rewrote my compiler from scratch.
I started implementing JSON Schema keywords one by one. Things were going well. I hit unevaluatedProperties and unevaluatedItems supposedly difficult keywords. Turned out easier than expected. 100% compliance.
I felt good.
Then I heard about the JSON Schema Test Suite.
God.
The Test Suite (Hell Has Levels)
Edge cases. Edge cases upon edge cases.
I questioned why I started. Multiple times. But I gritted my teeth and pushed through. I learned about compositions allOf, anyOf, oneOf. The not keyword. Conditionals. I implemented every single one. Fixed tons of edge cases.
Things were finally working.
Then I learned about $ref.
The $ref Nightmare (My Life Flashed Before My Eyes)
I thought $ref could only point to $defs or definitions. That's what I built for. I had a simple resolver find the definitions, extract them, store them in a map, generate function names, done.
Then I ran the test suite again.
Tests failed. The paths looked like #/properties/something.
What the hell is properties doing after #/?
My entire understanding was wrong. $ref can point to anything in the schema. Any path. Any fragment.
My life flashed before my eyes.
I knew what was coming.
Rewrite. Again. And Again.
I rewrote my resolver to traverse the entire schema. Collect all $refs. Generate unique function names. Assign them. It was working.
Then I learned about external references.
And internal $ids.
I rewrote my resolver again.
Then I found out external references could have fragments.
Patched it.
Then I discovered $anchor and $dynamicAnchor.
$dynamicAnchor (I Lost Hope in Life)
This is where I sacrificed half my vitality.
$dynamicAnchor is supposed to be dynamic resolved at runtime based on the dynamic scope. But I tried to resolve it at compile time. Because performance.
I studied the keyword. I studied the spec. I patched. And patched. And patched. Edge cases everywhere. I started questioning life itself.
External anchors. Anchor fragments. Recursive references. I handled all of it.
Eventually, it worked.
I even optimized my resolver to avoid traversing the schema twice. Things were finally stable.
Then I ran benchmarks again.
AJV was destroying me.
The Inlining Arc
Refs. Compositions. oneOf. Conditionals. AJV was faster on all of them.
I wondered what magic they were using. Turns out: inlining.
So I started inlining $refs that don't have circular references. Tons of patching. Missed edge cases. Fixed them. It worked.
Now I was beating AJV on refs.
Then I moved to compositions. Inlining allOf, anyOf, oneOf. This was not easy. More patches. More bugs. More fixes.
But I got there.
I implemented allErrors mode. Added support for $data references. Custom error messages. Custom formats. Custom keywords. Standalone code generation.
Then I made optimizations, hoisting regex objects outside functions, inlining contains, hoisting ref functions.
Then I ran the final benchmarks.
The Results
I won.
Here's the summary:
| Metric | JetValidator | AJV | Winner |
|---|---|---|---|
| Avg Compilation | 1.47ms | 28.29ms | JetValidator (19x faster) |
| Valid Data Wins | 36/62 | 26/62 | JetValidator (58%) |
| Invalid Data Wins | 45/62 | 17/62 | JetValidator (73%) |
| Overall Win Rate | β | β | JetValidator (72%) |
19x faster compilation. That's not a marginal improvement. That's a different league.
Why 19Γ compilation speed actually changes things:
- Serverless cold starts: from seconds β near-instant
- CLI tools & dev hot-reload: schema changes feel immediate
- Dynamic / user-provided schemas: no painful startup lag
- Edge functions / Vercel/Netlify: lower execution time bills
Check out the main github readme to reproduce benchmarks, you must be willing to wait a while thoughπ
What I Built
@jetio/validator β The core validator. 26kb gzipped. Zero dependencies. TypeScript-first.
- Full JSON Schema support (Draft 06, 07, 2019-09, 2020-12)
- 99.5%+ JSON Schema Test Suite compliance
- 19x faster compilation than AJV (sub-millisecond in many cases)
- Fast validation too, not just compilation
- Built-in format validators (no extra packages)
- Custom formats support
- Custom error messages (
errorMessagekeyword included) -
$datareferences for cross-field validation -
elseIfkeyword (JSON Schema spec extension) - Custom keywords (macro, compile, validate, code)
- Standalone code generation
- Meta-schema CLI
-
allErrorsmode
@jetio/schema-builder - A fluent, type-safe API for building JSON Schemas.
Why I didn't just use AJV:
The feature set was inspired by AJV, but I wanted to fix some pain points I had:
- No schema builder
- Nested
if/else/thengets messy (addedelseIfto fix this) - Formats and custom errors require external packages
- Documentation was hard to navigate
- I wanted something purely original Why I didn't just use AJV:
The feature set was inspired by AJV, but I wanted to fix some pain points I had:
- No schema builder
- Nested
if/else/thengets messy (addedelseIfto fix this) - Formats and custom errors require external packages
- Documentation was hard to navigate
- I wanted something purely original
For those who want the speed of JetValidator but also want great developer experience. Stop writing JSON schemas by hand. Get autocomplete. Get type safety. Get the elseIf keyword that JSON Schema should have had years ago.
const userSchema = new SchemaBuilder()
.object()
.properties({
name: s => s.string().minLength(2),
age: s => s.number().minimum(18)
})
.required(['name', 'age'])
.build();
Links
- GitHub (Validator): github.com/official-jetio/validator
- GitHub (Schema Builder): github.com/official-jetio/schema-builder
- npm (Validator): @jetio/validator
- npm (Schema Builder): @jetio/schema-builder
- Full Documentation: https://github.com/official-jetio/validator/blob/main/DOCUMENTATION.md
- Github Page:https://official-jetio.github.io
- Benchmarks: Full Comparison | Interactive
Benchmark contains detailed information like device, condition methodology and more.
A Note on AJV
The compilation approach was inspired by AJV. I heard it used code generation, so I took a similar path, but simpler, using string concatenation.
I never looked at AJV's codebase. I didn't want to be influenced. I built blind, from the spec, from the test suite, from trial and error.
The custom error messages, $data support, custom formats, custom keywords all inspired by AJV's feature set.
AJV is great. It paved the way. It's battle-tested, widely used, and the maintainers have done incredible work.
I learned from it. I respected it.
And then I built something faster.
All in all, JSON schema is definitely not for the weak.
Common questions:
- 100% spec compliant? β 99.5%+ compliance on JSON Schema Test Suite (Draft 06, 07, 2019-09, 2020-12). Higher than AJV on several drafts.
- Production ready? β Built for my own backend framework. Early days, but battle-tested against 3,000+ official test cases. Feedback welcome.
- AJV migration? β Similar API. Minimal code changes needed.
Read full documentation for thorough understanding. DOCUMENTATION.md
Final Words
I didn't set out to dethrone anyone. I was just building a backend framework and needed validation. One thing led to another. Weeks turned into months. Rewrites turned into rewrites of rewrites.
The biggest win in all of this is the compilation speed, as its proof of a fundamentally different architecture that chose simplicity.
I questioned why I started more times than I can count. $dynamicAnchor almost broke me. The test suite almost broke me. But I kept going.
And now I'm here.
AJV has been the king for years.
I came for the crown.
β Drop a star if you're curious: github.com/official-jetio/validator
Who's sticking with AJV? Who's trying this? Honest feedback welcome - roast me if the numbers are wrong.
Want great DX too? npm install @jetio/schema-builder
Top comments (0)