ata-validator
1.61.3 · 29 Oct 2026MIT21.8k / weekRepositoryHomepagenpmAtom feed
1.61.3 is the newest release of 1.x.
In short
1 new5 changed
Release note
A performance release, prompted by a benchmark from Jason Desrosiers that ran the official suite through prebuilt validators and read only .valid. On that shape ata went from 778 ns to about 150 ns per call: errors are now materialized on first read instead of on every rejection, the interpreted engine compiles schemas into a tree of plain closures (no new Function, so it holds under a CSP), and the verdict path carries no error machinery at all. Error output is unchanged, held byte-for-byte by differential tests over 2,864 suite cases.
Added
A closure-tree compiler for the interpreted engine. A schema the code generator declines is compiled into a tree of plain closures, one per schema node, with every keyword branch decided at compile time and
$reftargets resolved once; no source generation and nonew Function, so it works under a CSP and on Workers. Scope: schemas withoutunevaluatedProperties/unevaluatedItems, and$dynamicRefonly in single-resource schemas; everything else keeps the generic evaluator.tests/test_plan_compiler.jsholds the compiled tree to byte-identical verdicts and errors against the evaluator over 2,864 suite cases.
Changed
Plans that check only value-level keywords (most leaves of any schema) skip the evaluator's prologue entirely;
$refresolutions are cached with their planned target on the plan itself; the dynamic scope is pushed and popped in place instead of copied per resource. Rejection results are a small class with theerrorsaccessor on the prototype, since defining a getter inside an object literal builds a closure and an accessor property on every rejection, which was the single largest cost on the rejection path.The interpreted engine gained a verdict-only mode:
isValidObject()and the internal fast checks walk the schema without constructing a single error object, message string or scratch array. On an interpreter-routed schema the boolean check dropped from 894 ns to 70 ns.Object validation for
$dynamicRefschemas no longer routes to the native engine. The interpreted engine has scored the same on every$dynamicRefcase of the suite since the dynamic-scope fix in 1.7.0, needs no addon, and carries the verdict-only mode; the suite's$dynamicRefrejects dropped from about 1.7 µs to the interpreter's cost.String length bounds decide from the UTF-16 length where possible: a string's code point count always sits between half its length and its length, so
minLength/maxLengthonly count code points inside the narrow band where the answer is genuinely uncertain. The surrogate test is a single wraparound compare. The code generator already worked this way; the interpreter and the closure path now match it.Errors are paid for when read, not when produced.
validate()answers the verdict from the fastest engine for the schema and materializeserrorsthrough a cached getter on first access; declaration-order sorting and enrichment (received value, suggestions, source frames) moved with it into one presentation layer. A caller that only reads.valid, which is every gateway check, no longer pays for error construction at all. The output of.errorsis byte-for-byte what it was. Measured on a suite-shaped benchmark of prebuilt validators over 1,052 mixed valid and invalid cases,validate().validwent from 778 ns to about 150 ns per call, ahead of every error-capable validator we measured, and a rejection that never has its errors read now costs less thanabortEarlymode used to. One observable edge: mutating the data betweenvalidate()and the first read of.errorsnow reflects the mutated data in the errors, and if the mutation makes the data valid the errors fall back to a single generic entry.