ata-validator 1.17.2 → 1.18.1
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/README.md +19 -3
- package/index.js +2 -1
- package/lib/aot-impl.js +11 -0
- package/lib/draft7.js +45 -1
- package/lib/schema-scan.js +9 -0
- package/lib/version.js +1 -1
- package/package.json +9 -9
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 |
|
|
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
|
-
|
|
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
|
@@ -10,7 +10,7 @@ const {
|
|
|
10
10
|
compileToJSCodegenWithErrors,
|
|
11
11
|
compileToJSCombined,
|
|
12
12
|
} = require("./lib/js-compiler");
|
|
13
|
-
const { normalizeDraft7, normalizeNullable, stripFormatAssertions } = require("./lib/draft7");
|
|
13
|
+
const { normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions } = require("./lib/draft7");
|
|
14
14
|
const { enabledKeywords, stripDisabledKeywords } = require("./lib/vocabularies");
|
|
15
15
|
const { needsNormalization } = require("./lib/schema-scan");
|
|
16
16
|
const { isV1Dialect } = require("./lib/dialect");
|
|
@@ -618,6 +618,7 @@ function _normalizeCallerSchema(s, inheritDraft7) {
|
|
|
618
618
|
const copy = _deepCloneWithSymbols(s)
|
|
619
619
|
if (needsDraft7) normalizeDraft7(copy, true)
|
|
620
620
|
normalizeNullable(copy)
|
|
621
|
+
normalizeExclusiveBounds(copy)
|
|
621
622
|
// Return original when normalization produced no change, copy otherwise.
|
|
622
623
|
// Kept even though the walk has already said there is work, so that a walk
|
|
623
624
|
// which over-reports still returns exactly what it returned before.
|
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/draft7.js
CHANGED
|
@@ -105,6 +105,50 @@ function _normalize(schema) {
|
|
|
105
105
|
// 'null' (`{ type: 'X', nullable: true }` -> `{ type: ['X', 'null'] }`), the
|
|
106
106
|
// same shape AJV produces under its `nullable` option. Recurses only through
|
|
107
107
|
// schema-bearing keywords so it never touches data values (default, const, etc.).
|
|
108
|
+
|
|
109
|
+
// draft-04 spelled the exclusive bounds as booleans modifying `minimum` and
|
|
110
|
+
// `maximum`; every dialect since spells them as the bound itself. The engines
|
|
111
|
+
// read the numeric form, and the codegen path compared the value against the
|
|
112
|
+
// boolean directly, so `true` became 1: a schema saying "below 5, exclusive"
|
|
113
|
+
// rejected 1 and a schema saying "above 3, exclusive" accepted 3. Rewriting
|
|
114
|
+
// the pair here gives every engine the same numeric keyword, which is also
|
|
115
|
+
// what makes the two agree.
|
|
116
|
+
function normalizeExclusiveBounds(schema) {
|
|
117
|
+
_walkExclusive(schema, new Set())
|
|
118
|
+
return schema
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function _walkExclusive(node, seen) {
|
|
122
|
+
if (typeof node !== 'object' || node === null) return
|
|
123
|
+
if (Array.isArray(node)) {
|
|
124
|
+
for (const item of node) _walkExclusive(item, seen)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
if (seen.has(node)) return
|
|
128
|
+
seen.add(node)
|
|
129
|
+
|
|
130
|
+
if (typeof node.exclusiveMinimum === 'boolean') {
|
|
131
|
+
if (node.exclusiveMinimum === true && typeof node.minimum === 'number') {
|
|
132
|
+
node.exclusiveMinimum = node.minimum
|
|
133
|
+
delete node.minimum
|
|
134
|
+
} else {
|
|
135
|
+
// `false` restates the inclusive bound, and a boolean with no bound
|
|
136
|
+
// beside it says nothing at all.
|
|
137
|
+
delete node.exclusiveMinimum
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (typeof node.exclusiveMaximum === 'boolean') {
|
|
141
|
+
if (node.exclusiveMaximum === true && typeof node.maximum === 'number') {
|
|
142
|
+
node.exclusiveMaximum = node.maximum
|
|
143
|
+
delete node.maximum
|
|
144
|
+
} else {
|
|
145
|
+
delete node.exclusiveMaximum
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const key of Object.keys(node)) _walkExclusive(node[key], seen)
|
|
150
|
+
}
|
|
151
|
+
|
|
108
152
|
function normalizeNullable(schema) {
|
|
109
153
|
if (typeof schema !== 'object' || schema === null) return schema
|
|
110
154
|
_normalizeNullable(schema)
|
|
@@ -190,4 +234,4 @@ function _stripFormat(schema, seen) {
|
|
|
190
234
|
}
|
|
191
235
|
}
|
|
192
236
|
|
|
193
|
-
module.exports = { isDraft7, normalizeDraft7, normalizeNullable, stripFormatAssertions }
|
|
237
|
+
module.exports = { isDraft7, normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions }
|
package/lib/schema-scan.js
CHANGED
|
@@ -27,6 +27,7 @@ const ANCHOR_ID = 1 << 2; // a fragment-only `$id`, which is an anchor in
|
|
|
27
27
|
const DEFINITIONS = 1 << 3; // `definitions`, renamed to `$defs`
|
|
28
28
|
const DEPENDENCIES = 1 << 4; // `dependencies`, split into two keywords
|
|
29
29
|
const TUPLE_ITEMS = 1 << 5; // array-valued `items`, split into `prefixItems`
|
|
30
|
+
const EXCLUSIVE_BOOL = 1 << 6; // draft-04's boolean `exclusiveMinimum`/`exclusiveMaximum`
|
|
30
31
|
|
|
31
32
|
// Everything only draft-07 normalization acts on. A schema of another dialect
|
|
32
33
|
// can carry these without them meaning anything, so they are read together
|
|
@@ -74,6 +75,9 @@ function scan(schema) {
|
|
|
74
75
|
if (node.definitions !== undefined && node.$defs === undefined) bits |= DEFINITIONS;
|
|
75
76
|
if (node.dependencies !== undefined) bits |= DEPENDENCIES;
|
|
76
77
|
if (Array.isArray(node.items)) bits |= TUPLE_ITEMS;
|
|
78
|
+
if (typeof node.exclusiveMinimum === 'boolean' || typeof node.exclusiveMaximum === 'boolean') {
|
|
79
|
+
bits |= EXCLUSIVE_BOOL;
|
|
80
|
+
}
|
|
77
81
|
if (typeof node.$id === 'string' && ANCHOR_ID_RE.test(node.$id)) bits |= ANCHOR_ID;
|
|
78
82
|
|
|
79
83
|
// `Object.keys` rather than `for...in`: the prototype chain has nothing
|
|
@@ -99,6 +103,10 @@ function scan(schema) {
|
|
|
99
103
|
function needsNormalization(schema, isDraft7) {
|
|
100
104
|
const bits = scan(schema);
|
|
101
105
|
if (bits & NULLABLE) return true;
|
|
106
|
+
// The boolean form of the exclusive bounds is draft-04's, and it is
|
|
107
|
+
// unambiguous wherever it appears: no later dialect gives a boolean any
|
|
108
|
+
// meaning there. Normalizing it is not gated on the declared dialect.
|
|
109
|
+
if (bits & EXCLUSIVE_BOOL) return true;
|
|
102
110
|
return isDraft7 ? (bits & DRAFT7_WORK) !== 0 : false;
|
|
103
111
|
}
|
|
104
112
|
|
|
@@ -111,5 +119,6 @@ module.exports = {
|
|
|
111
119
|
DEFINITIONS,
|
|
112
120
|
DEPENDENCIES,
|
|
113
121
|
TUPLE_ITEMS,
|
|
122
|
+
EXCLUSIVE_BOOL,
|
|
114
123
|
DRAFT7_WORK,
|
|
115
124
|
};
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.1",
|
|
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_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",
|
|
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_exclusive_bounds.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.
|
|
126
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
127
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
128
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
129
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
130
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
131
|
-
"@ata-validator/native-win32-x64": "1.
|
|
125
|
+
"@ata-validator/native-darwin-arm64": "1.18.1",
|
|
126
|
+
"@ata-validator/native-darwin-x64": "1.18.1",
|
|
127
|
+
"@ata-validator/native-linux-arm64-gnu": "1.18.1",
|
|
128
|
+
"@ata-validator/native-linux-arm64-musl": "1.18.1",
|
|
129
|
+
"@ata-validator/native-linux-x64-gnu": "1.18.1",
|
|
130
|
+
"@ata-validator/native-linux-x64-musl": "1.18.1",
|
|
131
|
+
"@ata-validator/native-win32-x64": "1.18.1"
|
|
132
132
|
},
|
|
133
133
|
"peerDependencies": {
|
|
134
134
|
"yaml": "^2.0.0"
|