ata-validator 1.5.0 → 1.6.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,25 @@
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.6.0 - 2026-08-08
6
+
7
+ ### Added
8
+
9
+ - The JSON Schema v1 dialect. A schema declaring `"$schema": "https://json-schema.org/v1"`, or the dated `https://json-schema.org/v1/2026` the specification repository's meta-schema carries, is now validated under v1 rather than under 2020-12. The difference ata implements is `$dynamicRef`: v1 removes the bookending requirement, so a reference resolves through the dynamic scope whether or not the schema it initially lands on carries a matching `$dynamicAnchor`, and also when it resolves to nothing on its own. The outermost matching anchor still in scope wins, as before. `propertyDependencies`, the other v1 addition, shipped in 1.5.0.
10
+ - Against the suite's `v1` directory with nothing excluded, ata scores 1123 of 1127. The four it misses are the same four that fail on 2020-12: one `$dynamicRef` scope corner each engine misses, a definition validated against the meta-schema, and two remote-reference cases. `npm run test:suite` now runs the dialect alongside 2020-12 and draft 7, `tests/test_no_eval.js` runs it with code generation blocked (1124 of 1127 there), and `tests/test_v1_dialect.js` checks the switch itself: the same document must not validate the same way under both dialects.
11
+ - Only `$dynamicRef` routing changes. Everything else ata implements is identical under v1 and 2020-12, so a v1 schema that does not use the keyword takes the same compiled path it always did. One that does use it validates on the interpreted engine, since the JS compiler and the native addon both resolve the 2020-12 way. The native engine does not implement bookending at all, which is invisible to the official 2020-12 suite but means it cannot be trusted to answer for either dialect here.
12
+
13
+ ## 1.5.1 - 2026-08-07
14
+
15
+ ### Fixed
16
+
17
+ - `isValid()` on a buffer disagreed with `validate()` on the parsed value for 294 of 2208 cases in the official suite, in both directions. Two causes: the path returned the code generator's `false` directly, which is ambiguous between "invalid" and "the plan stopped at a composition opcode and the walker should finish", so every schema using `allOf`, `anyOf`, `oneOf` or `$ref` was rejected outright; and it ended in a second, simpler walker that had drifted from the one `validate()` uses. It now calls the same walker with all errors off, so there is one set of semantics rather than two kept in step by hand. The disagreement drops to 243 cases, the rest being the on-demand plan answering before the walker runs, which is engine work rather than a setting.
18
+ - `tests/test_buffer_path_parity.js` records that number so it cannot widen, and the README and the edge runtimes guide now state the gap, since `isValid`, `countValid` and `batchIsValid` are shipped APIs and a caller has no way to know otherwise.
19
+
20
+ ### Changed
21
+
22
+ - `ATA_NO_MIMALLOC` skips the bundled `mimalloc-new-delete.h` include. A toolchain that ships the mimalloc headers along with its own `operator new`/`delete` over the same allocator hits a duplicate symbol at link time; Emscripten with `-sMALLOC=mimalloc` is that case, so the source could not be compiled to WebAssembly at all. The native build does not define it and is unchanged.
23
+
5
24
  ## 1.5.0 - 2026-08-05
6
25
 
7
26
  ### Added
package/README.md CHANGED
@@ -22,6 +22,10 @@ npm install ata-validator --omit=optional
22
22
 
23
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 scores the same on the official suite as the native one, 1285 of 1290 Draft 2020-12 cases; the two miss a different `$dynamicRef` scope corner each. 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
24
 
25
+ Those four also do not yet agree with `validate()`. Over the official suite they differ on 243 of 2208 cases, in both directions, concentrated in `unevaluatedProperties`, `contains`, `const` and the `$ref` family. `npm test` measures the gap on every run so it cannot widen, and `docs/edge-runtimes.md` has the detail. Until it is closed, use them where throughput matters more than exactness, and use `validate()` or `isValidObject()` as the check on untrusted input.
26
+
27
+ Where `new Function` is refused altogether, on Cloudflare Workers, Deno Deploy or under a strict Content-Security-Policy, ata drops to the interpreted engine and scores 1286 of 1290 with code generation blocked. No flags, and on Workers no `nodejs_compat` either. See [docs/edge-runtimes.md](docs/edge-runtimes.md).
28
+
25
29
  In your code:
26
30
 
27
31
  ```ts
@@ -457,8 +461,17 @@ Copy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See
457
461
  | Enum/Const | `enum`, `const` |
458
462
  | Composition | `allOf`, `anyOf`, `oneOf`, `not` |
459
463
  | Conditional | `if`, `then`, `else` |
460
- | References | `$ref`, `$defs`, `definitions`, `$id` |
464
+ | References | `$ref`, `$defs`, `definitions`, `$id`, `$anchor`, `$dynamicRef`, `$dynamicAnchor` |
461
465
  | Boolean | `true`, `false` |
466
+ | v1 | `propertyDependencies` |
467
+
468
+ ### Dialects
469
+
470
+ `$schema` selects the dialect. Draft 2020-12 is the default, draft-07 is normalized on the way in, and `https://json-schema.org/v1` (or the dated `https://json-schema.org/v1/2026`) selects JSON Schema v1.
471
+
472
+ Two things differ under v1. `propertyDependencies` selects a subschema by the value of a property rather than by its presence, which is what `dependentSchemas` does. And `$dynamicRef` no longer requires bookending: the reference resolves through the dynamic scope whether or not the schema it first lands on carries a matching `$dynamicAnchor`, so the outermost matching anchor still in scope wins. Everything else ata implements is identical under both dialects, so a schema that declares no `$schema` behaves exactly as before.
473
+
474
+ Both are implemented in the interpreted engine, so a v1 schema that uses `$dynamicRef` validates there rather than through the compiler or the native addon, which resolve the 2020-12 way. Schemas that use neither keyword take the same compiled path they always did.
462
475
 
463
476
  ### Format Validators (hand-written, no regex)
464
477
 
@@ -466,12 +479,12 @@ Copy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See
466
479
 
467
480
  ### Known limitations
468
481
 
469
- Running the whole Draft 2020-12 suite with nothing excluded, `format` and `default` under specification semantics (`assertFormat: false`, `useDefaults: false`), gives 1285 of 1290 cases, 99.6%. Draft 7 gives 911 of 922, 98.8%. `npm run test:suite` reproduces both and lists the remaining failures by name.
482
+ Running the whole Draft 2020-12 suite with nothing excluded, `format` and `default` under specification semantics (`assertFormat: false`, `useDefaults: false`), gives 1285 of 1290 cases, 99.6%. Draft 7 gives 911 of 922, 98.8%. The v1 dialect gives 1123 of 1127, 99.6%, and the four it misses are the same four that fail on 2020-12. `npm run test:suite` reproduces all three and lists the remaining failures by name.
470
483
 
471
484
  Areas that remain deliberate scope decisions for 1.x:
472
485
 
473
486
  - **Remote `$ref` over the network** is not fetched. Register cross-schema refs explicitly with `schemas: [...]` or `addSchema()`.
474
- - **`$vocabulary`** is not implemented; custom vocabularies are ignored rather than enforced.
487
+ - **`$vocabulary`** is not implemented; custom vocabularies are ignored rather than enforced. The keyword was extracted from the specification for the stable release as incomplete, so it is not part of v1.
475
488
  - **`contentEncoding` / `contentMediaType` / `contentSchema`** are annotation-only, as the spec permits, and are not validated.
476
489
  - **`minLength`/`maxLength`** count UTF-16 code units, not grapheme clusters.
477
490
  - **Infinite-loop detection** relies on a recursion depth guard that cuts off circular `$ref` chains.
package/index.js CHANGED
@@ -10,6 +10,7 @@ const {
10
10
  compileToJSCombined,
11
11
  } = require("./lib/js-compiler");
12
12
  const { normalizeDraft7, normalizeNullable, stripFormatAssertions } = require("./lib/draft7");
13
+ const { isV1Dialect } = require("./lib/dialect");
13
14
  const { classify } = require("./lib/shape-classifier");
14
15
  const { buildTier0Plan, tier0Validate } = require("./lib/tier0");
15
16
 
@@ -724,11 +725,22 @@ class Validator {
724
725
  const cached = this._userFormats ? null : _compileCache.get(mapKey);
725
726
  let jsFn, jsCombinedFn, jsErrFn, _isCodegen = false;
726
727
  var _forceNapi = typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI;
727
- // Where source cannot be turned into a function, neither JS path is usable.
728
- // The closure path does not call `new Function` itself, so it survives the
729
- // block and would quietly handle schemas it gets wrong; the interpreted
730
- // engine is both eval-free and more correct, so go straight there.
731
- if (!codegenAvailable()) {
728
+ // v1 removes the bookending requirement for $dynamicRef. Only the
729
+ // interpreted engine implements that; the JS compiler and the native
730
+ // engine both resolve the 2020-12 way, so a v1 schema using the keyword
731
+ // goes to the interpreter rather than being validated under the wrong
732
+ // dialect. Schemas without $dynamicRef are unaffected: v1 and 2020-12
733
+ // agree on everything else ata implements.
734
+ this._v1Dynamic =
735
+ isV1Dialect(schemaObj) &&
736
+ (this._schemaStr.includes('"$dynamicRef"') || this._schemaStr.includes('"$dynamicAnchor"'));
737
+ //
738
+ // Where source cannot be turned into a function, neither JS path is usable
739
+ // either. The closure path does not call `new Function` itself, so it
740
+ // survives the block and would quietly handle schemas it gets wrong; the
741
+ // interpreted engine is both eval-free and more correct, so go straight
742
+ // there.
743
+ if (this._v1Dynamic || !codegenAvailable()) {
732
744
  jsFn = null; jsCombinedFn = null; jsErrFn = null;
733
745
  } else if (cached && !_forceNapi) {
734
746
  jsFn = cached.jsFn;
@@ -812,6 +824,7 @@ class Validator {
812
824
  _interp = createInterpreter(schemaObj, {
813
825
  schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
814
826
  formats: this._userFormats,
827
+ v1: isV1Dialect(schemaObj),
815
828
  });
816
829
  }
817
830
  const r = _interp.validate(d);
@@ -1065,7 +1078,7 @@ class Validator {
1065
1078
  // using it goes there even when it also uses $dynamicRef.
1066
1079
  const _hasPropDeps = this._schemaStr.includes('"propertyDependencies"')
1067
1080
  let _validate;
1068
- if (_hasDynRef && !_hasUneval && !_hasPropDeps) {
1081
+ if (_hasDynRef && !_hasUneval && !_hasPropDeps && !this._v1Dynamic) {
1069
1082
  // validateJSON is the C++ path with full anchor-map support; the NAPI
1070
1083
  // direct V8 `validate` path has no anchor maps.
1071
1084
  _validate = (data) => this._compiled.validateJSON(JSON.stringify(data));
@@ -1076,6 +1089,7 @@ class Validator {
1076
1089
  const interp = createInterpreter(schemaObj, {
1077
1090
  schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
1078
1091
  formats: this._userFormats,
1092
+ v1: isV1Dialect(schemaObj),
1079
1093
  });
1080
1094
  _validate = (data) => interp.validate(data);
1081
1095
  this.validateJSON = (jsonStr) => {
@@ -1133,6 +1147,7 @@ class Validator {
1133
1147
  const interp = createInterpreter(schemaObj, {
1134
1148
  schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
1135
1149
  formats: this._userFormats,
1150
+ v1: isV1Dialect(schemaObj),
1136
1151
  });
1137
1152
  const run = preprocess
1138
1153
  ? (data) => { preprocess(data); return interp.validate(data); }
package/lib/dialect.js ADDED
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ // Dialect detection.
4
+ //
5
+ // Only the v1 dialect is distinguished here, and only because v1 changes
6
+ // behavior that is otherwise identical to 2020-12: the bookending requirement
7
+ // for `$dynamicRef` is removed. Everything else ata does under 2020-12 is
8
+ // unchanged under v1, so a schema that declares no `$schema` keeps the
9
+ // existing default rather than being guessed at.
10
+ //
11
+ // The official test suite states the dialect as `https://json-schema.org/v1`.
12
+ // The meta-schema in the specification repository carries the dated
13
+ // `https://json-schema.org/v1/2026`. Both name the same dialect, and
14
+ // `draft/next` is the name the same work carried before the stable release was
15
+ // numbered, so all three are accepted.
16
+ const V1_DIALECTS = new Set([
17
+ 'https://json-schema.org/v1',
18
+ 'https://json-schema.org/v1/schema',
19
+ 'https://json-schema.org/v1/2026',
20
+ 'https://json-schema.org/draft/next/schema',
21
+ ]);
22
+
23
+ function isV1Dialect(schema) {
24
+ if (typeof schema !== 'object' || schema === null) return false;
25
+ if (typeof schema.$schema !== 'string') return false;
26
+ // A trailing '#' is an empty fragment naming the same document.
27
+ const uri = schema.$schema.endsWith('#') ? schema.$schema.slice(0, -1) : schema.$schema;
28
+ return V1_DIALECTS.has(uri);
29
+ }
30
+
31
+ module.exports = { isV1Dialect, V1_DIALECTS };
@@ -265,6 +265,9 @@ class Interpreter {
265
265
  this.root = rootSchema;
266
266
  this.state = indexSchemas(rootSchema, opts.schemaMap);
267
267
  this.userFormats = opts.formats || null;
268
+ // 2020-12 requires bookending for `$dynamicRef`; v1 removes the
269
+ // requirement. See the `$dynamicRef` branch in eval().
270
+ this.bookending = !opts.v1;
268
271
  this.patternCache = new Map();
269
272
  }
270
273
 
@@ -337,11 +340,19 @@ class Interpreter {
337
340
  const ref = schema.$dynamicRef;
338
341
  let { node, base: refBase } = resolveRef(ref, base, this.state);
339
342
  const [, fragment] = splitFragment(resolveUri(base, ref));
340
- if (node !== undefined && fragment && !fragment.startsWith('/')) {
341
- // Initial target must itself carry the matching $dynamicAnchor for
342
- // dynamic resolution; otherwise it behaves like $ref.
343
+ if (fragment && !fragment.startsWith('/')) {
344
+ // Under 2020-12 the initial target must itself carry the matching
345
+ // $dynamicAnchor before the dynamic scope is consulted (bookending);
346
+ // otherwise the keyword behaves like $ref. v1 removes that
347
+ // requirement, so the scope is searched whether or not the initial
348
+ // target carries the anchor, and also when the reference does not
349
+ // resolve on its own, which is the shape the suite uses: a
350
+ // `$dynamicRef` in a resource that declares no anchor of that name.
343
351
  const initialDyn = this.state.dynamicAnchors.get(refBase);
344
- if (initialDyn && initialDyn.get(fragment) === node) {
352
+ const bookended = node !== undefined && initialDyn && initialDyn.get(fragment) === node;
353
+ if (bookended || !this.bookending) {
354
+ // Outermost scope first: the first matching $dynamicAnchor
355
+ // encountered while evaluating wins.
345
356
  for (const scopeBase of dynScope) {
346
357
  const dyn = this.state.dynamicAnchors.get(scopeBase);
347
358
  if (dyn && dyn.has(fragment)) {
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.5.0';
10
+ module.exports = '1.6.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -44,9 +44,9 @@
44
44
  "release:check": "node scripts/regen-safe-regex-source.js && node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_source_sync.js && node tests/test_version_sync.js",
45
45
  "build": "cmake-js build --target ata",
46
46
  "rebuild": "cmake-js rebuild --target ata",
47
- "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_format_engine_parity.js && node tests/test_defs_pointer_alias.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_order.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
47
+ "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_format_engine_parity.js && node tests/test_defs_pointer_alias.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_order.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
48
48
  "bench:size": "node benchmark/bench_aot_size.mjs",
49
- "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7",
49
+ "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
50
50
  "test:compat": "node tests/test_compat.js",
51
51
  "test:standard-schema": "node tests/test_standard_schema.js",
52
52
  "test:browser": "node tests/test_browser.js",
@@ -112,12 +112,12 @@
112
112
  "LICENSE"
113
113
  ],
114
114
  "optionalDependencies": {
115
- "@ata-validator/native-darwin-arm64": "1.5.0",
116
- "@ata-validator/native-linux-x64-gnu": "1.5.0",
117
- "@ata-validator/native-linux-arm64-gnu": "1.5.0",
118
- "@ata-validator/native-linux-x64-musl": "1.5.0",
119
- "@ata-validator/native-linux-arm64-musl": "1.5.0",
120
- "@ata-validator/native-win32-x64": "1.5.0"
115
+ "@ata-validator/native-darwin-arm64": "1.6.0",
116
+ "@ata-validator/native-linux-x64-gnu": "1.6.0",
117
+ "@ata-validator/native-linux-arm64-gnu": "1.6.0",
118
+ "@ata-validator/native-linux-x64-musl": "1.6.0",
119
+ "@ata-validator/native-linux-arm64-musl": "1.6.0",
120
+ "@ata-validator/native-win32-x64": "1.6.0"
121
121
  },
122
122
  "peerDependencies": {
123
123
  "yaml": "^2.0.0"