Packages

ata-validator

1.61.3 · 29 Oct 2026MIT21.8k / weekRepositoryHomepagenpmAtom feed

Your version

1.7.1

23 Aug 2026Where it was published

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 $ref targets resolved once; no source generation and no new Function, so it works under a CSP and on Workers. Scope: schemas without unevaluatedProperties/unevaluatedItems, and $dynamicRef only in single-resource schemas; everything else keeps the generic evaluator. tests/test_plan_compiler.js holds 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; $ref resolutions 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 the errors accessor 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 $dynamicRef schemas no longer routes to the native engine. The interpreted engine has scored the same on every $dynamicRef case of the suite since the dynamic-scope fix in 1.7.0, needs no addon, and carries the verdict-only mode; the suite's $dynamicRef rejects 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/maxLength only 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 materializes errors through 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 .errors is byte-for-byte what it was. Measured on a suite-shaped benchmark of prebuilt validators over 1,052 mixed valid and invalid cases, validate().valid went 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 than abortEarly mode used to. One observable edge: mutating the data between validate() and the first read of .errors now reflects the mutated data in the errors, and if the mutation makes the data valid the errors fall back to a single generic entry.