JSON Schema validator
Check a JSON document against a JSON Schema
draft 2020-12 schema, or compile the schema into its own WebAssembly
validator. Everything runs in this page and nothing is uploaded. Both are QIP
components written in Zig:
Both pass all 1,298 cases of the official
JSON-Schema-Test-Suite
for draft 2020-12.
Remote schemas for $ref (optional)
The page validates as you type. Valid and Invalid say whether the
document conforms. If the schema itself can't be used, the validator stops
with an error instead of calling the document invalid. That happens with
invalid JSON, a $ref that doesn't resolve, an unsupported regular
expression or an unknown required vocabulary. So a broken schema never looks
like a bad document.
Compile schema to Wasm turns the schema into a standalone validator and
offers it as a download. The page then checks the document with the compiled
module too and says whether it agrees.
What it supports #
- All of draft 2020-12's core, applicator, unevaluated and validation
vocabularies, including
$dynamicRef, unevaluatedProperties and
unevaluatedItems. The 2020-12 meta-schemas are built in.
format and content* are annotations, as the default 2020-12 dialect
says, so they never make a document invalid.
- Remote
$refs are preloaded, never fetched. Pass them as "schemas",
keyed by absolute URI, the same way ata-validator's schemas option and
@cfworker/json-schema's addSchema work. Documents that declare $id are
also found by that $id.
- Numbers are compared exactly by their decimal text, never as
floating-point.
1.0 equals 1, 0.0075 is a multiple of 0.0001, and
1e400 is greater than 1e308. JavaScript validators can disagree on
numbers that doubles can't tell apart.
pattern uses ECMA-262 syntax with the u flag: literals, ., ^,
$, classes, \d \w \s \b and their negations, \p{Letter}, groups,
alternation and greedy or lazy quantifiers. Matching takes linear time, so
a pattern can't cause catastrophic backtracking. Lookaround and
backreferences are rejected as unsupported.
Patterns match anywhere in the string, as the specification says, so
[a-z]+ also accepts abc<script>. Write ^[a-z]+$ to match the whole
string.
Correctness #
The Compliance oracle json-schema-2020-12.comply.wasm packages
the JSON-Schema-Test-Suite's 1,298 draft 2020-12 core cases, without the
optional/ tests. The same oracle drives the QIP components and, through a
small adapter, two JavaScript libraries.
| Validator | Passed | |
QIP json-schema-2020-12-must-be-valid | 1,298 / 1,298 | 100.00% |
QIP compiled with json-schema-to-wasm | 1,298 / 1,298 | 100.00% |
| ata-validator 1.62.1 | 1,298 / 1,298 | 100.00% |
| @cfworker/json-schema 4.1.4 | 1,257 / 1,298 | 96.84% |
cfworker always asserts format (15 cases), resolves $dynamicRef like
$ref in most cases (22), and misses 4 other edge cases.
The oracle passes only when an invalid case is cleanly rejected. A validator
that fails by crashing on a schema doesn't count as rejecting the document.
A second oracle, json-schema-2020-12-interoperable-regex.comply.wasm, holds
the 1,292 cases whose patterns stay inside the spec's
interoperable regular expression subset,
plus . and escaped syntax characters such as \.. It's for validators that
accept only that subset.
Speed #
Node v26.3.0 on Apple Silicon, fastest of 31 runs, each workload in a fresh
process. Read the ratios rather than the exact times.
| Workload | QIP | ata-validator | @cfworker/json-schema |
| Whole suite, each schema compiled and used once | 1.77 ms | 19.8 ms | 39.2 ms |
| 0.75 MB document, schema compiled each time | 6.18 ms | 2.03 ms | 24.5 ms |
| 0.75 MB document, schema already compiled, from bytes | 1.96 ms | 2.03 ms | — |
| 0.75 MB document, schema already compiled, from a parsed object | — | 0.52 ms | 22.6 ms |
- Many small schemas, each used once: the one-call validator is about 11
times faster than ata and 22 times faster than cfworker. ata generates and
compiles JavaScript for every schema, and that is most of its cost here.
- One schema used many times: compile it with
json-schema-to-wasm.
Compiling the order schema takes about 0.1 ms. The compiled module then
validates the 0.75 MB document from raw bytes in 1.96 ms, level with ata
parsing the same bytes. Its own JSON parsing takes about 0.6 ms of that,
faster than JSON.parse on the same bytes (1.5 ms).
- Already-parsed objects: ata is faster when the document is already a
JavaScript object, because it skips parsing. A Wasm validator always reads
bytes.
The benchmark harness is in benchmarks/json-schema-2020-12/ in the qip
repository.
Use it outside the browser #
The one-call validator takes a JSON envelope with the schema, the document
and any remote schemas, in any key order. When the document conforms, it
outputs the document's exact bytes, so it can sit in a pipeline:
printf '{"schema": {"type": "array", "items": {"type": "integer"}}, "instance": [1, 2, 3]}' \
| npx @qip.dev/qipx qip.dev run application/json/json-schema-2020-12-must-be-valid.wasm
To reuse a schema, compile it once. The compiler takes the same envelope
without "instance". The second command prints [1, 2, 3], and the third
fails because "x" isn't an integer:
printf '{"schema": {"type": "array", "items": {"type": "integer"}}}' \
| npx @qip.dev/qipx qip.dev run -o ints.wasm application/json/json-schema-to-wasm.wasm
printf '[1, 2, 3]' | npx @qip.dev/qipx run ints.wasm
printf '[1, "x"]' | npx @qip.dev/qipx run ints.wasm
qipx downloads each component from qip.dev the first time and saves it at
the same path, such as application/json/json-schema-to-wasm.wasm. The
compiled ints.wasm is a local file, so those two commands need no host.
The compiled module runs in any QIP host: the browser, Node, Go, Rust and
Swift. It keeps the schema it checks in a custom section named
qip.json-schema. The compiler checks every keyword and pattern up front, so
a broken schema fails when you compile it, not when the first document
reaches the broken part.
Each component reads up to 4 MiB of input. Rejection carries no detail: it
says the document doesn't conform, not where. To find the failing part,
validate pieces of the document against parts of the schema, or use a
JavaScript library that reports errors.
See JSON components for other
application/json components.