ata-validator
1.61.3 · 29 Oct 2026MIT21.8k / weekRepositoryHomepagenpmAtom feed
1.61.3 is the newest release of 1.x.
In short
2 fixed12 changed
Release note
Standalone modules can carry the runtime's one-pass error function, the package starts faster and holds less per validator, the generators decline a subschema they produced no code for, and the supply chain is tightened: a tracked lockfile, pinned tools, attested release tarballs, and one place each for dynamic code and environment reads.
Measured against 1.46.1 on an Apple M4 Pro, Node 25.2.1, pure JS:
A Hono route on Bun to its first served request through the runtime API, from
benchmark/bundle: 9.1 to 7.6 ms, against 3.5 for the same app with no validation;Cold start (require, compile, first validate): 8.5 to 7.0 ms;
requirealone 6.4 to 4.6 ms, since the error generators are read on the first error compile and the format and regex sources on the first schema that needs them;Heap retained per validator that has validated, two thousand validators of a ten-key schema: 9.5 to 8.3 KB;
A served request, hot validation, the suite harness and the official suite: unchanged within noise;
Modules from
ata buildwithout--one-pass: byte for byte the same; with it, on a 175 KB schema and a 15 KB document, a rejected document in 39 µs where the older collector took 89.
Costs: the browser runtime bundle 125.2 to 128.0 KB gzipped (the one-pass emitter and the generator check); ata build over twenty schemas 94 to 99 ms, since a module's emission compiles the verdict in full where the validator kept only the light form.
Fixed
The code generators decline a subschema that constrains something and produced no code for it, instead of reading "no lines" as "holds". Every place that turns a subschema into lines (a nested verdict, a branch verdict, the inline annotation verdicts of run-time
unevaluatedPropertiesandunevaluatedItems, the three entry points) now checks that a schema with a validating keyword produced at least one line; a schema that trips it runs on the interpreted engine. This is the shape of the silent acceptances the generator has produced through$refhandling, and of the simplest form of the 1.46.0 one (with that fix removed, the check alone keeps{ anyOf: [{}, { additionalProperties: { const: true } }] }rejecting). It cannot see a check dropped from a subschema that produced other lines; the differential test below is what catches those.formatand an already-knowntypeare not counted, and unknown keywords are not either. No schema of the official suite, the SchemaStore corpus or the differential tests changed engine.The unevaluated differential test now compares
validate()after enough reads that it is the one-pass function, runs every third schema through a standalone module (with and without the one-pass error path), and generates branches that are empty or carryadditionalPropertiesat their root. With the 1.46.0 fix removed and the generator check disabled, it reports the acceptance.
Changed
Every place the package turns generated source into a function goes through one module,
lib/compile-fn.js, instead of callingnew Functionin seven files, and every environment variable the package honours is read throughlib/env.js, which lists them. Nothing changes in what runs; a reader auditing what the package evaluates, or a supply-chain scanner reporting it, finds one site. Where dynamic code is blocked the fallback to the interpreted engine is as before (tests/test_no_eval.js).A test bundles the Node entry with esbuild and runs the bundle (
tests/test_node_bundle.js): the native engine is found through seven optional packages of which at most one is installed on any machine, so the loader must require the platform package by a computed name, and a literal require of each, tried in this cycle, stopped the bundle of every app that imports the package.Supply chain: the package's own
package-lock.jsonis tracked and CI, the prebuilds and the publish install withnpm ci; the npm the publish job updates itself to is pinned; the Bowtie image is pinned by digest. A new workflow attaches to every GitHub release the packed tarball, a Sigstore build provenance attestation for it and a checksum file, so a tarball taken from a release verifies withgh attestation verify. On the repository, Dependabot alerts and security updates, secret scanning and push protection are on.tests/test_version_sync.jsnow also holds the lockfile to the package version, so a release bump that forgetsnpm install --package-lock-onlyfails the release check.A validator retains 8.3 KB of heap instead of 9.5 once it has validated (two thousand validators of a ten-key schema, each with its own schema, measured with
benchmark/bench_memory.cjs). The compiled verdict function carried, for as long as it lived, the generated source and the factories that build the tiered forms from it, 2.2 of the 3.4 KB it retained; they are now compiled in full only when asked for, at the tier a validator reaches after its first calls or when a build emits it, from the schema again. A validator that gets there pays one more compile, a few hundred microseconds once; one that answers a handful of requests, or only verdicts, never does. Schemas with custom keywords compile in full at once, so a keyword's compile form still runs once per schema.The error generator and the one-pass generator moved out of
lib/js-compiler.jsintolib/js-compiler-errors.js, read on the first call that compiles a validator's errors. They were 45 percent of the generator's source, parsed by every process at require; a verdict-only process never reads them now, and one that reads errors parses them when it does. The two files share the generator's helpers through an internal object; nothing in the package's interface changes.The format checkers' source (
lib/formats-source.js) and the linear regex engine (lib/safe-regex.js) are read on the first schema that needs them, aformator apattern, instead of with the compiler. Requiring the package costs 10 percent less (6.25 to 5.61 ms, pure JS, medians of 25 interleaved cold processes); a first validation against a schema with a format or a pattern pays about 0.3 ms of that back once.Standalone modules can report errors with the runtime's one-pass function:
toStandaloneModule(schema, { onePass: true })orata compile --one-pass. The one-pass function is whatvalidate()runs once a validator's errors have been read, one walk that checks and collects with a verdict per subtree, written into the module with its closure values as declarations. The older collector walked everything and built every path: on a 175 KB SchemaStore schema and a 15 KB document with two errors, the module'svalidate()on the rejected document takes 39 µs with the one-pass function where the older collector took 89 (a report from a config validator measured 2 to 2.7x against the default validator on 84 to 234 KB schemas and guessed at the position pass, which costs 11 of those). Not the default: the one-pass module is 1.3 to 1.7 times the older one, gzipped, since it carries a verdict per subtree and a counting copy per branch. Where the older collector declines a schema (unevaluated*next to a$ref), the one-pass function is taken without asking: 83 more schemas of the official suite get a module with real errors where they got the stub. The module's program is written without the counting copies the runtime uses to score oneOf and anyOf branches (a second copy of each branch's error body); when no branch holds, every branch's error function runs and the closest is picked from their errors, which measured the same on rejected documents and takes 5 to 12 percent off the module. Errors carry the same fields as before (code, keyword, instancePath, schemaPath, params, message, docUrl) and come in schema order, as the runtime's do. A module built with--sourceframes, or from a validator with a custom format, keeps the older collector.