ata-validator 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
4
4
 
5
+ ## 1.2.0 - 2026-07-18
6
+
7
+ ### Added
8
+
9
+ - An interpreted engine (`lib/interpreter.js`) now backs schemas the JS compiler cannot represent when the native addon is absent (browser, edge workers, `ATA_NO_NATIVE=1`). These schemas validated only with the native engine before, and 1.1.0 made them throw a clear error in native-less environments; they now just validate. Full draft 2020-12 semantics: `$id`/`$anchor` resolution, `$dynamicRef` dynamic scoping, annotation tracking for `unevaluatedProperties`/`unevaluatedItems`. The pure-JS configuration now passes 1184 of 1190 applicable cases (99.5%) in the official test suite, up from 974, and the six remaining failures are shared with the native engine. Error results on the native-less path also carry full per-keyword detail now instead of a single generic message.
10
+
11
+ ### Changed
12
+
13
+ - Validation errors now follow the schema's keyword declaration order instead of a fixed required-first order: a schema declaring `properties` before `required` reports the property errors first, matching what schema authors read top to bottom and what the previous default validator emitted. Order within one keyword is unchanged (`required` errors still follow the array). Single-error and `abortEarly` results are untouched. With this change ata passes every applicable test in Fastify's validation suite (181 of 187; the remaining 6 test the incumbent validator's own extension API rather than validation behavior).
14
+
15
+ ## 1.1.0 - 2026-07-18
16
+
17
+ ### Added
18
+
19
+ - TypeBox-style modifier combinators on `ata-validator/t`: `t.pick`, `t.omit`, `t.partial`, `t.required`, `t.composite`, and `t.recursive`. All six emit plain JSON Schema, so `Infer`, the runtime validator, and the AOT pipeline consume them with no adapter. This closes the authoring-parity gap for TypeBox migrations. Note: `t.recursive` schemas validate through the interpreted engine and are not AOT-precompilable; the other five combinators AOT-compile like any schema.
20
+
21
+ ### Changed
22
+
23
+ - The `ata-validator` package is now pure JavaScript: no bundled binaries, no vendored C++ sources, no install script. The native engine moved to per-platform `@ata-validator/native-*` packages, installed automatically as optional dependencies (the same pattern Vite uses for esbuild). The tarball shrinks from ~5.3 MB to under 300 KB. `npm install --omit=optional` or `ATA_NO_NATIVE=1` gives a guaranteed zero-binary setup; validation behavior is identical for every schema shape the JS engine compiles, and the few shapes that still need the native engine now throw a clear error instead (see Fixed below).
24
+
25
+ ### Fixed
26
+
27
+ - Schemas the JS engine cannot compile (some `$dynamicRef`, cyclic `$ref`, and unusual keyword interactions) crashed with `Maximum call stack size exceeded` in environments without the native addon: the lazy `validate` stub and the rich-error wrapper dispatched to each other forever. These schemas now throw a clear "not supported by the pure-JS engine" error on first use. The same cycle could hit `isValidObject` even with the native addon present when it was the first method called; it now falls through to the full compile and validates correctly.
28
+
5
29
  ## 1.0.2 - 2026-07-17
6
30
 
7
31
  ### Fixed
package/README.md CHANGED
@@ -14,6 +14,14 @@ npm install --save-dev ata-validator
14
14
  npx ata build 'schemas/*.json' --out-dir src/generated
15
15
  ```
16
16
 
17
+ The `ata-validator` package itself is pure JavaScript. The native accelerator (simdjson parsing, parallel NDJSON, buffer APIs) ships as per-platform optional packages that npm installs automatically where they fit, the same pattern Vite uses for esbuild. For a guaranteed zero-binary install:
18
+
19
+ ```bash
20
+ npm install ata-validator --omit=optional
21
+ ```
22
+
23
+ or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup passes 99.5% of the official draft 2020-12 test suite. Only the buffer and parallel APIs (`isValid` on raw buffers, `countValid`, `batchIsValid`, `validateAndParse`) need the native engine and say so with a clear error.
24
+
17
25
  In your code:
18
26
 
19
27
  ```ts
@@ -227,7 +235,14 @@ type User = Infer<typeof User>
227
235
  const v = new Validator(User)
228
236
  ```
229
237
 
230
- The builder covers primitives (`string`, `number`, `integer`, `boolean`, `null`), composites (`object` with `optional` keys, `array`, `tuple`, `record`, `union`, `intersect`, `literal`, `const`, `enum`), and refs (`ref`). Optionality is carried by a Symbol marker that the emitted JSON Schema and ata's codegen never see, so the output is still a plain JSON Schema literal that you can pass to anything that takes one.
238
+ The builder covers primitives (`string`, `number`, `integer`, `boolean`, `null`), composites (`object` with optional keys, `array`, `tuple`, `record`, `union`, `intersect`, `literal`, `const`, `enum`), plus the TypeBox-style modifiers `pick`, `omit`, `partial`, `required`, `composite`, and `recursive`. Optionality is carried by a Symbol marker that the emitted JSON Schema and ata's codegen never see, so the output is still a plain JSON Schema literal that you can pass to anything that takes one.
239
+
240
+ ```ts
241
+ const User = t.object({ id: t.integer(), name: t.string(), email: t.optional(t.string()) })
242
+ const Patch = t.partial(t.omit(User, ['id']))
243
+ type Patch = Infer<typeof Patch>
244
+ // { name?: string; email?: string }
245
+ ```
231
246
 
232
247
  #### Async refinement
233
248
 
@@ -462,6 +477,8 @@ If one of these blocks you, open an issue; scope decisions get revisited with re
462
477
 
463
478
  ## Building from Source
464
479
 
480
+ This section applies to contributors building the repository. Regular `npm install` users need not have a C++ toolchain.
481
+
465
482
  ### Development prerequisites
466
483
 
467
484
  Native builds require C/C++ toolchain support and the following libraries:
@@ -470,7 +487,7 @@ Native builds require C/C++ toolchain support and the following libraries:
470
487
  - `abseil`
471
488
  - `mimalloc`
472
489
 
473
- Install them before running `npm install` / `npm run build`:
490
+ Install them before running `npm run build`:
474
491
 
475
492
  ```bash
476
493
  # macOS (Homebrew)
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Native addon: optional. Core validate() uses JS codegen and works without it.
2
2
  // Buffer APIs (isValid, countValid, isValidParallel) require native.
3
- // Loading is delegated so this file stays free of `pkg-prebuilds`/`__dirname`/
4
- // `path` (the browser entry must not pull those in via the bundler).
3
+ // Loading is delegated to lib/native-load.js so this file stays free of
4
+ // platform probing and `path` (the browser entry must not pull those in).
5
5
  const native = require("./lib/native-load")();
6
6
  const {
7
7
  compileToJS,
@@ -319,6 +319,47 @@ function resolveSchemaByPath(rootSchema, schemaPath) {
319
319
  return target;
320
320
  }
321
321
 
322
+ // Rank an error by walking its schemaPath through the schema object: at each
323
+ // level the segment's index among the node's declared keys. Comparing ranks
324
+ // lexicographically orders errors by keyword declaration order, which is the
325
+ // order AJV emits and what schema authors read top to bottom. Segments that
326
+ // cannot be resolved (cross-schema refs, normalized keys) end the walk; the
327
+ // stable sort then keeps such errors in engine emission order.
328
+ function schemaOrderRank(rootSchema, schemaPath) {
329
+ if (!schemaPath || typeof schemaPath !== 'string' || !schemaPath.startsWith('#')) return null;
330
+ const parts = schemaPath.slice(1).split('/').filter(Boolean).map((s) => s.replace(/~1/g, '/').replace(/~0/g, '~'));
331
+ const rank = [];
332
+ let node = rootSchema;
333
+ for (const seg of parts) {
334
+ if (node == null || typeof node !== 'object') break;
335
+ if (Array.isArray(node)) {
336
+ const idx = Number(seg);
337
+ if (!Number.isInteger(idx) || idx < 0 || idx >= node.length) break;
338
+ rank.push(idx);
339
+ node = node[idx];
340
+ } else {
341
+ const idx = Object.keys(node).indexOf(seg);
342
+ if (idx < 0) break;
343
+ rank.push(idx);
344
+ node = node[seg];
345
+ }
346
+ }
347
+ return rank;
348
+ }
349
+
350
+ function sortErrorsBySchemaOrder(rootSchema, errors) {
351
+ const ranked = errors.map((e, i) => ({ e, i, rank: schemaOrderRank(rootSchema, e.schemaPath) }));
352
+ ranked.sort((a, b) => {
353
+ if (!a.rank || !b.rank) return a.i - b.i;
354
+ const n = Math.min(a.rank.length, b.rank.length);
355
+ for (let k = 0; k < n; k++) {
356
+ if (a.rank[k] !== b.rank[k]) return a.rank[k] - b.rank[k];
357
+ }
358
+ return a.i - b.i;
359
+ });
360
+ return ranked.map((r) => r.e);
361
+ }
362
+
322
363
  function parsePointerPath(path) {
323
364
  if (!path) return [];
324
365
  return path
@@ -569,6 +610,10 @@ class Validator {
569
610
  };
570
611
  } else {
571
612
  this._ensureCodegen();
613
+ // Codegen can bail on shapes it cannot represent; the full compile
614
+ // binds the native path or the unsupported thrower instead of
615
+ // leaving this stub to re-dispatch to itself.
616
+ if (!this._jsFn) this._ensureCompiled();
572
617
  }
573
618
  return this.isValidObject(data);
574
619
  };
@@ -721,16 +766,32 @@ class Validator {
721
766
  // invalid path doesn't dereference a null _compiled.
722
767
  const hasUnevaluated = schemaObj && (schemaObj.unevaluatedProperties !== undefined || schemaObj.unevaluatedItems !== undefined || this._schemaStr.includes('unevaluatedProperties') || this._schemaStr.includes('unevaluatedItems'))
723
768
  const hasDynRef = this._schemaStr.includes('"$dynamicRef"') || this._schemaStr.includes('"$dynamicAnchor"')
724
- const jsOnlyFallback = (d) => ({
725
- valid: jsFn(d),
726
- errors: jsFn(d) ? [] : [{
727
- keyword: 'validation',
728
- instancePath: '',
729
- schemaPath: '',
730
- params: {},
731
- message: 'schema validation failed (detailed errors unavailable without native addon)'
732
- }]
733
- });
769
+ // Native-less error path: the interpreted engine re-validates failing
770
+ // data to produce full errors. If it disagrees with the codegen verdict
771
+ // (it should not), a generic error keeps the result consistent.
772
+ let _interp = null;
773
+ const jsOnlyFallback = (d) => {
774
+ if (jsFn(d)) return { valid: true, data: d, errors: [] };
775
+ if (!_interp) {
776
+ const { createInterpreter } = require('./lib/interpreter');
777
+ _interp = createInterpreter(schemaObj, {
778
+ schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
779
+ formats: this._userFormats,
780
+ });
781
+ }
782
+ const r = _interp.validate(d);
783
+ if (!r.valid) return r;
784
+ return {
785
+ valid: false,
786
+ errors: [{
787
+ keyword: 'validation',
788
+ instancePath: '',
789
+ schemaPath: '',
790
+ params: {},
791
+ message: 'schema validation failed'
792
+ }]
793
+ };
794
+ };
734
795
  const errFn =
735
796
  safeErrFn ||
736
797
  (hasUnevaluated
@@ -1004,6 +1065,43 @@ class Validator {
1004
1065
  return valid;
1005
1066
  };
1006
1067
  }
1068
+ } else {
1069
+ // No JS codegen and no native engine: fall back to the interpreted
1070
+ // engine. Slow but correct, and strictly better than the previous
1071
+ // behavior (the lazy stubs re-dispatched to themselves forever).
1072
+ const { createInterpreter } = require('./lib/interpreter');
1073
+ const interp = createInterpreter(schemaObj, {
1074
+ schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
1075
+ formats: this._userFormats,
1076
+ });
1077
+ const run = preprocess
1078
+ ? (data) => { preprocess(data); return interp.validate(data); }
1079
+ : (data) => interp.validate(data);
1080
+ this.validate = run;
1081
+ this.isValidObject = (data) => run(data).valid;
1082
+ this.validateJSON = (jsonStr) => {
1083
+ try {
1084
+ return run(JSON.parse(jsonStr));
1085
+ } catch (e) {
1086
+ return { valid: false, errors: [{ keyword: 'syntax', instancePath: '', schemaPath: '#', params: {}, message: e.message }] };
1087
+ }
1088
+ };
1089
+ this.isValidJSON = (jsonStr) => this.validateJSON(jsonStr).valid;
1090
+ }
1091
+
1092
+ // Declaration-order errors: whichever engine produced them, multi-error
1093
+ // results are sorted by the schema's keyword declaration order before
1094
+ // enrichment. Single-error and abortEarly results pass through untouched.
1095
+ if (this.validate) {
1096
+ const inner = this.validate;
1097
+ const root = this._schemaObj;
1098
+ this.validate = (data) => {
1099
+ const result = inner(data);
1100
+ if (result && !result.valid && result.errors && result.errors.length > 1 && result !== ABORT_EARLY_RESULT) {
1101
+ return { valid: false, errors: sortErrorsBySchemaOrder(root, result.errors) };
1102
+ }
1103
+ return result;
1104
+ };
1007
1105
  }
1008
1106
 
1009
1107
  // richErrors enrichment: layered on top of whichever validate path was
package/lib/aot.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  // All ahead-of-time (AOT) code generation lives here so the default and browser
4
- // entries can stay free of `fs`, `path`, `__dirname`, and `pkg-prebuilds`.
4
+ // entries can stay free of `fs`, `path`, and `__dirname`.
5
5
  // Browser bundles get `lib/aot.browser.js` instead (see package.json `browser`
6
6
  // field), which throws if anyone tries to call an AOT function from the browser.
7
7
  //