Packages

ata-validator

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

Your version

1.61.0

26 Oct 2026Where it was published

1.61.3 is the newest release of 1.x.

In short

7 new2 fixed3 changed

Release note

Added

  • A schema with a default anywhere (ata fills defaults without being asked, so it has a preprocess pass) now builds its rich errors where they fail, as other schemas do: the path was closed to schemas with a preprocess pass, though it reads the document validate() has already processed. One required error on a four-field user with a defaulted newsletter went from 313 to 248 ns with its error list read; only the one-pass swap, which replaces validate() itself, stays off for such schemas. Found through the fastest-validator comparison in ata-core/validator-benchmarks (issue #74).

  • The Node entry warms the engine once the event loop is free: where a validator was built and is still untouched by then (not compiled, no method read off it), one check of a small schema runs, accepted and rejected, so that V8 compiles the generator's functions and the error builders before the first request instead of on it. On a four-field schema with the generator already loaded, a server's first verdict went from 3.1 to 3.4 ms to 0.5 to 0.6, the first error list from 6.7 ms to 0.8, at 1.5 to 2 ms of CPU once. A script that builds and checks in the same tick never runs it, the timer is unref'd, and ATA_NO_WARM=1 turns it off. The browser build has done the same since 1.57.0 (lib/tiering.js); the cold-start benchmarks, which check in the same tick as they build, are unchanged by it. tests/test_node_warm.js holds the conditions.

  • ata-validator/build exports bunTarget (the schema as a TypeScript type and the .bun.ts module ata build --target bun writes; with tags: true the split into a type with Bun's Schema tags and a residual schema, made for the typed validator the compiler's pull request carried until 2026-10-25) and verify (verifyOne, verifySource, generate), for a build plugin that replaces new Validator(schema) inside a Bun.build({ compile: { aot: true } }) and holds what it wrote against the runtime.

  • ata verify <glob> holds each schema's compiled module against a runtime Validator built from the schema now: documents made from the schema (--generate, 200 by default, half with one rule broken), the project's sample documents (--samples, JSON or JSON Lines files or a directory), verdicts and error lists compared, a module whose schema hash no longer matches the schema reported stale, a non-zero exit on any disagreement, stale or missing module, --json for machines. The check behind the fixes of 1.59.0 and 1.60.0, as a command a project runs in CI.

  • ata build --target bun writes a .bun.ts module beside the plain one for executables built with bun build --compile --aot: the schema as a TypeScript type (kinds, properties and which are required, items and fixed tuples, literals, local definitions as named types), isValid as a type guard and validate with the type on them, and parse(text), which declares the type where it parses (under the pull request build of 2026-10-25 that is no faster than a plain parse and the module: 564 to 574 ns against 570 to 578 on a ten-item order, so the file is a type for the program, not a faster path); the module checks the content, so every answer is the module's. A project points the compiler at the file with an "aot" condition in its package.json imports, which nothing else reads. A schema no type can be written for (a remote $ref, $dynamicRef, a root the type says nothing about) gets a warning, an error with --strict, and the plain module alone. The compiler's pull request carried its own typed validator, validate<T> from bun:bundle, until 2026-10-25, and this target was first written for it: the schema split into a type with Bun's Schema tags and a residual schema of the rules the tags do not carry, which on 1024 distinct ten-item orders under the pull request build of 2026-10-24 answered isValid in 96 to 99 ns against 108 for the plain module, and against the official suite never accepted a document the suite refuses (benchmark/proof/aot/suite-differential.mjs, 122 of 384 groups on 2020-12, 105 of 258 on draft 7). That validator left the pull request to return as a change of its own; bunTarget.schemaToBunType(schema, { tags: true }) in ata-validator/build still makes the split, and nothing in ata writes it until then.

  • A validator built without engine takes the interpreted engine where the process runs code compiled ahead of time: when Bun reports Bun.isAOT, or when ATA_ENGINE=interpreter is set. Under Bun's ahead-of-time compiler (pull request build) the generated code is interpreted and the interpreted engine is compiled: 80 against 530 ns a document for the same schema, measured in benchmark/proof/aot. engine: 'auto' by name keeps the generated code, and a module from ata build is unaffected, since it is compiled either way (34 ns there). tests/test_aot_engine.js.

  • schemaOf<T>(): a placeholder for the JSON Schema of a TypeScript type, written in at build time by @ata-project/unplugin from the type and the JSDoc tags on its properties, so new Validator(schemaOf<User>()) compiles like any static schema. It has no run-time form: a call that reaches the runtime throws with a message naming the plugin, rather than returning an empty schema that accepts everything. Exported from every entry.

Changed

  • The standalone module's error collector (the default form, with schema-source frames) reads each property once into a local, names a closed object's declared keys outright instead of a Set lookup per key when there are at most eight, and reuses the node's plain-object flag in that walk instead of reading the prototype again. A rejected ten-item order with its error list built went from 378 to 244 ns on Node (the one-pass form, 102, is unchanged and still opt-in for its size); the module's size does not move. Found by profiling line by line: one closedness walk held 31% of the samples.

  • ata build --target bun writes the one-pass error function by default and no schema-source frames, as an executable compiled ahead of time pays no bundle size: a rejected ten-item order with the error list built went from 724 to 189 ns under Bun's compiler (on Node, 378 to 105 against 99 for the runtime). The default for every other build stays the older collector with frames, since the one-pass form is 20% larger gzipped over 60 SchemaStore schemas (53 of them larger by more than 5%, the worst 1.6x); --one-pass and --no-source choose it by hand. The plugin's ahead-of-time path does the same.

  • Generated code walks and counts a closed object's keys in the form of the engine it is written for: for...in on V8, where it is the cheaper form, an indexed loop over Object.keys on JavaScriptCore, where for...in is a slow path. The runtime writes for the engine it runs on; ata build --target bun and the plugin's Bun entry write for JavaScriptCore. One form is written, so neither engine pays a branch per object (an earlier form with a load-time flag cost V8 5 to 7% on small schemas). A third form, jsc-aot, is what --target bun and the plugin's ahead-of-time path write: a closed object whose declared keys are all required is checked by reading Object.keys once and comparing it with the declared list in order, the order JSON.parse gives, with a membership walk when the order differs. Under Bun's compiler that put the plain module level with Bun's own validate<T> on a ten-item order (105 to 107 ns against 96 to 105, from 108 to 112); on Bun's JIT the same form loses 5 to 15 ns, which is why it is its own target. The prototype read picks __proto__ on V8 and Object.getPrototypeOf on JavaScriptCore at generation. Measured on a ten-item order: Bun 1.4.4's JIT 65 to 53 ns a document, Bun's ahead-of-time compiler (pull request build) 227 to 108, Node 25 unchanged.

Fixed

  • CompiledModule.validate in ata-validator/compiled declares errors as read-only, which is what a module returns for an accepted document (a frozen array); under Bun's ahead-of-time compiler, where a TypeScript error fails the build, a compiled-away validator in a TypeScript file did not build.

  • The standalone emitters required lib/aot-order.js and lib/error-runtime.js through a name held in a constant, to keep bundlers for runtimes without fs from following them. An executable compiled ahead of time (Bun) cannot load a module from a computed path and warned about each such require at build time. They are literal requires now, and the package's browser field keeps lib/aot-order.js out of browser and edge bundles instead; the browser, edge and lite bundles and the Workers proof are unchanged. The native engine's loader still tries its optional packages by name, inside a try, which is one warning left under that compiler.