ata-validator 1.17.1 → 1.18.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,12 @@
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.17.2 - 2026-09-13
6
+
7
+ ### Changed
8
+
9
+ - `new Validator(schema)` no longer walks the schema. Normalization (the draft-07 rewrites, `nullable`, `format` removal under `assertFormat: false`) and the scan that decides whether any of it is needed now run on the first read of the schema, which is the first compile. Construction is the instance and its fields. Measured on the schema-benchmarks product schema built fresh each time: 4.28 to 0.91 µs; with the `@ata-project/keywords` wrapper (0.3.1, which reads the schema lazily too) 6.70 to 2.81 µs. Nothing observable changes for a schema that is used: the same normalized document reaches the engines, and the identity cache, `keywords`, `assertFormat` and `source` options behave as before. `tests/test_lazy_normalization.js` counts property reads on the schema during construction and holds them at the dialect lookup alone.
10
+
5
11
  ## 1.17.1 - 2026-09-13
6
12
 
7
13
  ### Changed
package/README.md CHANGED
@@ -51,9 +51,25 @@ if (isValid(req.body)) {
51
51
 
52
52
  The `.compiled.mjs` modules are self-contained: zero runtime dependency on ata-validator, fully tree-shakeable, with TypeScript types emitted alongside.
53
53
 
54
+ ## Measured by others
55
+
56
+ Two public harnesses run ata without ata's involvement. Quote these before anything in this
57
+ file.
58
+
59
+ - [schemabenchmarks.dev](https://schemabenchmarks.dev), a benchmark of runtime validation
60
+ libraries maintained outside this project. On its validation page, valid data, the run of
61
+ 2026-09-13 against ata 1.14.1 puts ata first at 603 ns, with the next entry at 1.77 times
62
+ that. The same site puts ata last on the download page, at 64.9 KB gzipped, because the entry
63
+ it bundles is the runtime compiler; the module `ata build` emits for that schema is 3.9 KB
64
+ minified and gzipped, and a compiled entry for the harness is in preparation.
65
+ - [Bowtie](https://bowtie.report/), the cross-implementation JSON Schema test harness. ata's
66
+ harness runs Draft 2020-12 and draft 7 there; on the harness at ata 1.16.1 the official suite
67
+ passes with nothing failed, errored or skipped under Bowtie's own runner. The v1 dialect is
68
+ declared in a pending harness change that waits for a Bowtie release.
69
+
54
70
  ## Why AOT
55
71
 
56
- | Dimension | Schema | ata-AOT | AJV-runtime | Difference |
72
+ | Dimension | Schema | ata-AOT | runtime validator | Difference |
57
73
  |---|---|---|---|---|
58
74
  | Bundle (gzipped) | simple | 1.0 KB | 52.7 KB | 50.5x smaller |
59
75
  | Bundle (gzipped) | complex | 4.8 KB | 52.7 KB | 11.0x smaller |
@@ -62,8 +78,8 @@ The `.compiled.mjs` modules are self-contained: zero runtime dependency on ata-v
62
78
  | Throughput (1M ops) | simple | 258 Mops/s | 102 Mops/s | 2.5x faster |
63
79
  | Compile time | simple | 8 µs | 1.61 ms | 191x faster |
64
80
 
65
- Reproduce on your machine with `npm run bench:aot-vs-ajv`. Numbers from one run on Apple
66
- M4 Pro, Node 25.2.1, 2026-08-13. Across three runs throughput moved between 258 and 278
81
+ The runtime column is the default validator most frameworks ship. Reproduce on your machine
82
+ with `npm run bench:aot-vs-ajv`. Numbers from one run on Apple M4 Pro, Node 25.2.1, 2026-08-13. Across three runs throughput moved between 258 and 278
67
83
  Mops/s and the compile ratio between 150x and 199x, so treat the last two rows as an order
68
84
  of magnitude rather than a constant.
69
85
 
package/index.js CHANGED
@@ -797,6 +797,26 @@ function resolveSchemaForPreprocess(schema, schemaMap) {
797
797
  return cloned || s
798
798
  }
799
799
 
800
+ // `_schemaObj` and `_usesKeywords` are materialized together on first read:
801
+ // the caller's schema normalized on a clone (the caller's object is never
802
+ // touched), `format` stripped under `assertFormat: false`, and the custom
803
+ // keyword scan. The accessors then step aside for own data properties, so
804
+ // every later read is a plain field.
805
+ function _materializeSchema(self) {
806
+ const raw = self._rawSchema;
807
+ const options = self._options;
808
+ let schemaObj = _normalizeCallerSchema(raw);
809
+ const isCallers = self._rawIsCallers && schemaObj === raw;
810
+ if (options.assertFormat === false) {
811
+ schemaObj = stripFormatAssertions(isCallers ? _deepCloneWithSymbols(schemaObj) : schemaObj);
812
+ }
813
+ const usesKeywords = self._keywords !== null && schemaUsesKeywords(schemaObj, self._keywords);
814
+ Object.defineProperty(self, '_schemaObj', { value: schemaObj, writable: true, configurable: true, enumerable: true });
815
+ Object.defineProperty(self, '_usesKeywords', { value: usesKeywords, writable: true, configurable: true, enumerable: true });
816
+ Object.defineProperty(self, '_schemaIsCallers', { value: isCallers && schemaObj === raw, writable: true, configurable: true, enumerable: true });
817
+ return schemaObj;
818
+ }
819
+
800
820
  class Validator {
801
821
  constructor(schema, opts) {
802
822
  const options = opts || {};
@@ -809,22 +829,16 @@ class Validator {
809
829
  if (hit) return hit;
810
830
  }
811
831
 
832
+ // The schema is not walked here. Normalization (draft-07 rewrites,
833
+ // nullable, `assertFormat: false`) and the scan that decides whether any
834
+ // of it is needed run on the first read of `_schemaObj`, which is the
835
+ // first compile. Construction is the object and its fields; a server
836
+ // building a validator per request pays nothing for a schema it never
837
+ // uses, and a benchmark timing construction measures construction.
812
838
  // When schema is a string, JSON.parse already produces a fresh object.
813
- // When schema is an object, normalization runs on a clone so the caller's
814
- // object is never touched.
815
- let schemaObj = typeof schema === "string"
816
- ? _normalizeCallerSchema(JSON.parse(schema))
817
- : _normalizeCallerSchema(schema);
818
- const rootIsDraft7 = !!(schemaObj && typeof schemaObj === 'object' && typeof schemaObj.$schema === 'string' &&
819
- (schemaObj.$schema === 'http://json-schema.org/draft-07/schema#' || schemaObj.$schema === 'http://json-schema.org/draft-07/schema'));
820
-
821
- // assertFormat: false makes `format` annotation-only. Strip it on a clone
822
- // so the caller's schema keeps the keyword.
823
- if (options.assertFormat === false) {
824
- schemaObj = stripFormatAssertions(
825
- schemaObj === schema ? _deepCloneWithSymbols(schemaObj) : schemaObj,
826
- );
827
- }
839
+ const raw = typeof schema === "string" ? JSON.parse(schema) : schema;
840
+ const rootIsDraft7 = !!(raw && typeof raw === 'object' && typeof raw.$schema === 'string' &&
841
+ (raw.$schema === 'http://json-schema.org/draft-07/schema#' || raw.$schema === 'http://json-schema.org/draft-07/schema'));
828
842
 
829
843
  // Built here rather than below because `$vocabulary` is resolved against
830
844
  // it, and that resolution waits until compilation so a meta-schema
@@ -832,21 +846,17 @@ class Validator {
832
846
  const shared = buildSchemaMap(options.schemas, rootIsDraft7);
833
847
  const schemaMap = shared || new Map();
834
848
  this._schemaMapShared = shared !== null;
835
- this._schemaIsCallers = schemaObj === schema;
836
849
  this._vocabulariesApplied = false;
837
850
 
838
- // Custom keywords, normalized once. `_usesKeywords` is what routes the
839
- // schema to the interpreted engine and keeps it out of the shared
840
- // compile cache; a schema that registers keywords but uses none of them
841
- // takes the ordinary path.
851
+ // Custom keywords, normalized once. `_usesKeywords` (resolved with the
852
+ // schema) is what routes the schema to the interpreted engine and keeps
853
+ // it out of the shared compile cache; a schema that registers keywords
854
+ // but uses none of them takes the ordinary path.
842
855
  this._keywords = normalizeKeywords(options.keywords);
843
- this._usesKeywords = false;
844
- if (this._keywords !== null && schemaUsesKeywords(schemaObj, this._keywords)) {
845
- this._usesKeywords = true;
846
- }
847
856
 
848
857
  this._schemaStr = null; // lazy: computed on first use
849
- this._schemaObj = schemaObj;
858
+ this._rawSchema = raw;
859
+ this._rawIsCallers = typeof schema !== "string";
850
860
  this._options = options;
851
861
  this._noOpts = !opts;
852
862
  this._initialized = false;
@@ -2162,6 +2172,18 @@ function _defineLazyMethod(name, maker) {
2162
2172
  });
2163
2173
  }
2164
2174
 
2175
+ for (const [name, pick] of [
2176
+ ['_schemaObj', (self) => _materializeSchema(self)],
2177
+ ['_usesKeywords', (self) => { _materializeSchema(self); return self._usesKeywords; }],
2178
+ ['_schemaIsCallers', (self) => { _materializeSchema(self); return self._schemaIsCallers; }],
2179
+ ]) {
2180
+ Object.defineProperty(Validator.prototype, name, {
2181
+ configurable: true,
2182
+ get() { return pick(this); },
2183
+ set(v) { Object.defineProperty(this, name, { value: v, writable: true, configurable: true, enumerable: true }); },
2184
+ });
2185
+ }
2186
+
2165
2187
  _defineLazyMethod('validate', (self) => (data) => {
2166
2188
  self._ensureCompiled();
2167
2189
  return self.validate(data);
package/lib/aot-impl.js CHANGED
@@ -202,6 +202,17 @@ function emitClone(node, access, depth) {
202
202
  // An array of primitives has nowhere to hide a key. The reference is
203
203
  // shared with the input, exactly as a shallow strip would leave it.
204
204
  value = read;
205
+ } else if (prop.type === 'array' && prop.items && typeof prop.items === 'object' &&
206
+ !Array.isArray(prop.items) && prop.items.type === 'object' &&
207
+ prop.items.properties) {
208
+ // An array of objects: rebuild each element the same way, so unknown
209
+ // keys inside the array are dropped too. Most real schemas have one of
210
+ // these, and declining them left parse() emitted only for shapes nobody
211
+ // writes.
212
+ const el = '_e' + depth;
213
+ const inner = emitClone(prop.items, el, depth + 1);
214
+ if (!inner) return null;
215
+ value = `${read}.map((${el}) => (${inner}))`;
205
216
  } else {
206
217
  return null;
207
218
  }
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.17.1';
10
+ module.exports = '1.18.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.17.1",
3
+ "version": "1.18.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",
@@ -51,7 +51,7 @@
51
51
  "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",
52
52
  "build": "cmake-js build --target ata",
53
53
  "rebuild": "cmake-js rebuild --target ata",
54
- "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_buffer_gate.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.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_native_loaded.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_format_mode.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_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_formats_single_pass.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_pattern_properties_errors.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_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_plan_compiler.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_enrich_received.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_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.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",
54
+ "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_buffer_gate.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.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_native_loaded.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_format_mode.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_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_formats_single_pass.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_pattern_properties_errors.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_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_plan_compiler.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_enrich_received.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_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.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",
55
55
  "bench:size": "node benchmark/bench_aot_size.mjs",
56
56
  "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
57
57
  "test:compat": "node tests/test_compat.js",
@@ -122,13 +122,13 @@
122
122
  "LICENSE"
123
123
  ],
124
124
  "optionalDependencies": {
125
- "@ata-validator/native-darwin-arm64": "1.17.1",
126
- "@ata-validator/native-darwin-x64": "1.17.1",
127
- "@ata-validator/native-linux-arm64-gnu": "1.17.1",
128
- "@ata-validator/native-linux-arm64-musl": "1.17.1",
129
- "@ata-validator/native-linux-x64-gnu": "1.17.1",
130
- "@ata-validator/native-linux-x64-musl": "1.17.1",
131
- "@ata-validator/native-win32-x64": "1.17.1"
125
+ "@ata-validator/native-darwin-arm64": "1.18.0",
126
+ "@ata-validator/native-darwin-x64": "1.18.0",
127
+ "@ata-validator/native-linux-arm64-gnu": "1.18.0",
128
+ "@ata-validator/native-linux-arm64-musl": "1.18.0",
129
+ "@ata-validator/native-linux-x64-gnu": "1.18.0",
130
+ "@ata-validator/native-linux-x64-musl": "1.18.0",
131
+ "@ata-validator/native-win32-x64": "1.18.0"
132
132
  },
133
133
  "peerDependencies": {
134
134
  "yaml": "^2.0.0"