ata-validator 1.32.0 → 1.32.2
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 +44 -33
- package/index.js +29 -7
- package/lib/js-compiler.js +87 -26
- package/lib/tier0.js +3 -1
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -53,15 +53,24 @@ The `.compiled.mjs` modules are self-contained: zero runtime dependency on ata-v
|
|
|
53
53
|
|
|
54
54
|
## Measured by others
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
Public harnesses run ata without ata's involvement. Quote these before anything in this
|
|
57
57
|
file.
|
|
58
58
|
|
|
59
|
+
- The [runtime type benchmark](https://moltar.github.io/typescript-runtime-type-benchmarks/)
|
|
60
|
+
maintained by moltar, 44 to 56 libraries per test. In its official run on Node 24, with ata
|
|
61
|
+
1.32.0, ata is first of 45 on assertStrict at 37.2M operations a second, and its
|
|
62
|
+
ahead-of-time entry is first of 44 on parseStrict at 36.3M, with the runtime entry second.
|
|
63
|
+
On assertLoose ata is fifth of 56 and on parseSafe sixth of 45. Some of the entries ahead
|
|
64
|
+
of it on those two rows skip checks JSON Schema requires there, such as rejecting an array
|
|
65
|
+
where an object is declared or NaN where a number is. On Deno the same run puts ata between
|
|
66
|
+
12th and 23rd; 1.32.1 makes it about 2.5 times faster there, locally, and had not been
|
|
67
|
+
through the harness yet at the time of writing.
|
|
59
68
|
- [schemabenchmarks.dev](https://schemabenchmarks.dev), a benchmark of runtime validation
|
|
60
|
-
libraries
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
libraries. Its published run of 2026-09-25 still uses ata 1.29.0 and puts it sixth of 30 on
|
|
70
|
+
valid data at 870 ns and tenth on invalid data at 67 ns. Most of that time was the `uri`
|
|
71
|
+
format check and a wrapper in the keywords package, both rewritten since. Run locally on an
|
|
72
|
+
Apple M4 Pro, the same harness takes ata from 365 ns on 1.29.0 to 240 ns on 1.32.1 for valid
|
|
73
|
+
data, with invalid data at 21 ns on both; those are local figures, not the site's.
|
|
65
74
|
- [Bowtie](https://bowtie.report/), the cross-implementation JSON Schema test harness. ata's
|
|
66
75
|
harness runs Draft 2020-12 and draft 7 there; on the harness at ata 1.16.1 the official suite
|
|
67
76
|
passes with nothing failed, errored or skipped under Bowtie's own runner. The v1 dialect is
|
|
@@ -71,19 +80,20 @@ file.
|
|
|
71
80
|
|
|
72
81
|
| Dimension | Schema | ata-AOT | runtime validator | Difference |
|
|
73
82
|
|---|---|---|---|---|
|
|
74
|
-
| Bundle (gzipped) | simple | 1.
|
|
75
|
-
| Bundle (gzipped) | complex | 7.
|
|
76
|
-
| Bundle (gzipped) | nested | 4.
|
|
77
|
-
| Cold start | simple |
|
|
78
|
-
| Throughput (1M ops) | simple |
|
|
79
|
-
| Compile time | simple |
|
|
83
|
+
| Bundle (gzipped) | simple | 1.3 KB | 52.7 KB | 39.8x smaller |
|
|
84
|
+
| Bundle (gzipped) | complex | 7.8 KB | 52.7 KB | 6.7x smaller |
|
|
85
|
+
| Bundle (gzipped) | nested | 4.5 KB | 52.7 KB | 11.8x smaller |
|
|
86
|
+
| Cold start | simple | 22 ms | 48 ms | 2.2x faster |
|
|
87
|
+
| Throughput (1M ops) | simple | 307 Mops/s | 100 Mops/s | 3.1x faster |
|
|
88
|
+
| Compile time | simple | 17 µs | 1.62 ms | 93x faster |
|
|
80
89
|
|
|
81
90
|
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-09-
|
|
83
|
-
on ata-validator 1.
|
|
84
|
-
is
|
|
85
|
-
between
|
|
86
|
-
rows as an order of
|
|
91
|
+
with `npm run bench:aot-vs-ajv`. Numbers from one run on Apple M4 Pro, Node 25.2.1, 2026-09-27,
|
|
92
|
+
on ata-validator 1.32.1. Emitted modules are about 0.2 to 0.4 KB larger gzipped than on 1.25.0,
|
|
93
|
+
which is what the exact own-property checks added in 1.31.2 cost. Across three runs throughput
|
|
94
|
+
moved between 296 and 307 Mops/s against 99 to 107, cold start between 21 and 22 ms against 41
|
|
95
|
+
to 48, and the compile ratio between 87x and 95x, so treat the last three rows as an order of
|
|
96
|
+
magnitude rather than a constant.
|
|
87
97
|
|
|
88
98
|
The wins are largest on bundle size and compile time because AOT moves work from runtime to
|
|
89
99
|
build time. Throughput and cold start are also faster because the compiled validator is a
|
|
@@ -211,19 +221,20 @@ is the most common way to get a misleading number out of this library.
|
|
|
211
221
|
|
|
212
222
|
| | compiled with `ata build` | runtime `new Validator(schema)` |
|
|
213
223
|
|---|---|---|
|
|
214
|
-
| In a bundle, gzipped | **
|
|
215
|
-
| Time to a served request | **3.4 ms** |
|
|
224
|
+
| In a bundle, gzipped | **2.3 KB** | 91.4 KB |
|
|
225
|
+
| Time to a served request | **3.4 ms** | 11.2 ms |
|
|
216
226
|
| Schema known when | build time | any time |
|
|
217
227
|
|
|
218
|
-
The bundle row is
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
228
|
+
The bundle row is the ten-field user schema in `tests/fixtures/error-dx/user.schema.json`,
|
|
229
|
+
every export of the compiled module against `new Validator(schema)`, built with
|
|
230
|
+
`bun build --minify --target=browser` on ata 1.32.1. The startup row is a Hono route on
|
|
231
|
+
Bun 1.4, best of seven, from `benchmark/bundle`, against 3.5 ms for the same app doing no
|
|
232
|
+
validation at all, so the compiled path costs nothing measurable to start. The runtime
|
|
222
233
|
figure is what it is because a schema that arrives at run time can use any keyword, so
|
|
223
234
|
the whole engine has to be there. The compiled module imports nothing and contains only
|
|
224
235
|
the checks your schema asks for.
|
|
225
236
|
|
|
226
|
-
**On a server, use whichever fits your schemas.**
|
|
237
|
+
**On a server, use whichever fits your schemas.** 91 KB of JavaScript on a Node or Bun
|
|
227
238
|
process is not a cost anyone notices, and the runtime API is the simpler thing to reach
|
|
228
239
|
for. Speed is the same either way once warm.
|
|
229
240
|
|
|
@@ -262,7 +273,7 @@ const v = new Validator({
|
|
|
262
273
|
required: ['name', 'email']
|
|
263
274
|
});
|
|
264
275
|
|
|
265
|
-
// Fast boolean check
|
|
276
|
+
// Fast boolean check, compiled to JS
|
|
266
277
|
v.isValidObject({ name: 'Mert', email: 'mert@example.com', age: 26 }); // true
|
|
267
278
|
|
|
268
279
|
// Full validation with error details + defaults applied
|
|
@@ -279,7 +290,7 @@ v.isValid(Buffer.from('{"name": "Mert", "email": "mert@example.com"}'));
|
|
|
279
290
|
// Which engine answers this schema: 'codegen', 'closure', 'native' or 'interpreter'
|
|
280
291
|
v.engine(); // 'codegen'
|
|
281
292
|
|
|
282
|
-
// Parallel batch
|
|
293
|
+
// Parallel batch, multi-core, NDJSON
|
|
283
294
|
const ndjson = Buffer.from(lines.join('\n'));
|
|
284
295
|
v.isValidParallel(ndjson); // bool[]
|
|
285
296
|
v.countValid(ndjson); // number
|
|
@@ -455,7 +466,7 @@ const v = new Validator(schema, {
|
|
|
455
466
|
|
|
456
467
|
### Build-time compile (`ata compile`)
|
|
457
468
|
|
|
458
|
-
The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser.
|
|
469
|
+
The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser. For the ten-field user schema in `tests/fixtures/error-dx/user.schema.json` the module is 2.6 KB gzipped, full error detail included, against 91 KB for the runtime bundled for the browser.
|
|
459
470
|
|
|
460
471
|
```bash
|
|
461
472
|
npx ata compile schemas/user.json -o src/generated/user.validator.mjs
|
|
@@ -496,14 +507,14 @@ npx ata build 'schemas/*.json' --out-dir build/validators --check
|
|
|
496
507
|
|
|
497
508
|
Run with `--watch` during development for incremental rebuilds.
|
|
498
509
|
|
|
499
|
-
Bundle sizes for
|
|
500
|
-
`bun build --minify --target=browser
|
|
510
|
+
Bundle sizes for the 10-field user schema in `tests/fixtures/error-dx/user.schema.json`,
|
|
511
|
+
minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.32.1:
|
|
501
512
|
|
|
502
513
|
| What the app imports | Size | Notes |
|
|
503
514
|
|---|---|---|
|
|
504
|
-
| `Validator` from `ata-validator` |
|
|
505
|
-
| `isValid` from the compiled module | **1.
|
|
506
|
-
| `validate` from the compiled module | **
|
|
515
|
+
| `Validator` from `ata-validator` | 91.3 KB | The compiler ships with it, because a runtime schema can use any keyword |
|
|
516
|
+
| `isValid` from the compiled module | **1.2 KB** | Nothing else is reachable, so the error collector is dropped |
|
|
517
|
+
| `validate` from the compiled module | **2.1 KB** | Adds the detailed error collector |
|
|
507
518
|
|
|
508
519
|
`--abort-early` makes the generated source about three times smaller, and after
|
|
509
520
|
bundling it makes no difference: importing only `isValid` already leaves the error
|
|
@@ -526,7 +537,7 @@ through a `setFormats()` export the module carries. `docs/API.md` has the
|
|
|
526
537
|
details.
|
|
527
538
|
|
|
528
539
|
**Fastify startup, 10 route schemas, from a cold process to the first validated request:
|
|
529
|
-
ajv
|
|
540
|
+
ajv 20.2 ms, ata 3.9 ms, no build step required.** ata registers in 0.2 ms of that and
|
|
530
541
|
compiles on the first request, so counting only registration would overstate the gap.
|
|
531
542
|
Reproduce with `node benchmark/bench_fastify_boot.mjs`.
|
|
532
543
|
|
package/index.js
CHANGED
|
@@ -371,6 +371,9 @@ const _identityCache = new WeakMap();
|
|
|
371
371
|
|
|
372
372
|
const SIMDJSON_PADDING = 64;
|
|
373
373
|
const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
374
|
+
// How many calls a validator answers through its verdict function before its
|
|
375
|
+
// validate() and validateJSON() compile the single-function hybrid.
|
|
376
|
+
const HYBRID_TIER_CALLS = 64;
|
|
374
377
|
const ABORT_EARLY_RESULT = Object.freeze({
|
|
375
378
|
valid: false,
|
|
376
379
|
errors: Object.freeze([Object.freeze({
|
|
@@ -1543,8 +1546,20 @@ class Validator {
|
|
|
1543
1546
|
if (combined) { impl = combined; return _mustReject(combined(data)); }
|
|
1544
1547
|
return errOnly(data);
|
|
1545
1548
|
};
|
|
1546
|
-
|
|
1547
|
-
|
|
1549
|
+
// Tiered: the first calls go through the verdict function and the
|
|
1550
|
+
// resolver, and the hybrid is compiled once the validator is in use.
|
|
1551
|
+
// Compiling it up front was a second parse of the whole schema on
|
|
1552
|
+
// every validator, most of which answer a handful of requests or only
|
|
1553
|
+
// verdicts. A refused compile stays on the first tier.
|
|
1554
|
+
let warm = 0;
|
|
1555
|
+
const first = (data) => {
|
|
1556
|
+
if (++warm >= HYBRID_TIER_CALLS && impl === first) {
|
|
1557
|
+
impl = jsFn._hybridFactory(VALID_RESULT, onReject) || ((d) => (jsFn(d) ? VALID_RESULT : onReject(d)));
|
|
1558
|
+
return impl(data);
|
|
1559
|
+
}
|
|
1560
|
+
return jsFn(data) ? VALID_RESULT : onReject(data);
|
|
1561
|
+
};
|
|
1562
|
+
impl = first;
|
|
1548
1563
|
const run = (data) => impl(data);
|
|
1549
1564
|
this.validate = preprocess
|
|
1550
1565
|
? (data) => { preprocess(data); return run(data); }
|
|
@@ -1613,11 +1628,18 @@ class Validator {
|
|
|
1613
1628
|
// it validates and collects in one pass, and the error generator behind
|
|
1614
1629
|
// it. `errPreferCombined` is that order, resolved on the first rejection
|
|
1615
1630
|
// instead of at compile time.
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
const jsonValidateInner =
|
|
1620
|
-
|
|
1631
|
+
// Tiered the same way as validate() above.
|
|
1632
|
+
let jsonHybrid = null;
|
|
1633
|
+
let jsonWarm = 0;
|
|
1634
|
+
const jsonValidateInner = (obj) => {
|
|
1635
|
+
if (jsonHybrid !== null) return jsonHybrid(obj);
|
|
1636
|
+
if (++jsonWarm >= HYBRID_TIER_CALLS) {
|
|
1637
|
+
jsonHybrid = (jsFn._hybridFactory && jsFn._hybridFactory(VALID_RESULT, errPreferCombined))
|
|
1638
|
+
|| ((o) => (jsFn(o) ? VALID_RESULT : errPreferCombined(o)));
|
|
1639
|
+
return jsonHybrid(obj);
|
|
1640
|
+
}
|
|
1641
|
+
return jsFn(obj) ? VALID_RESULT : errPreferCombined(obj);
|
|
1642
|
+
};
|
|
1621
1643
|
// Parsed text takes the same preprocess pass as a parsed object, so
|
|
1622
1644
|
// validate(obj) and validateJSON(text) answer the same for the same
|
|
1623
1645
|
// document. Without it, coercion, removal and defaults applied on one
|
package/lib/js-compiler.js
CHANGED
|
@@ -918,9 +918,28 @@ const PROTO_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '_
|
|
|
918
918
|
// and reading the errors of a product document went from 3.9 to 5.8 us. The
|
|
919
919
|
// helper only runs once `in` has said yes, and on an ordinary object it
|
|
920
920
|
// answers from the prototype without looking at the key.
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
921
|
+
//
|
|
922
|
+
// `__proto__` is read through the accessor on Object.prototype, which Deno
|
|
923
|
+
// removes. There every read came back undefined, every object took the per-key
|
|
924
|
+
// hasOwnProperty path, and a ten-field check went from 2.5 to 40 ns. Where the
|
|
925
|
+
// accessor is missing the prototype comes from Object.getPrototypeOf instead,
|
|
926
|
+
// chosen by a constant the engine folds, so Node pays nothing for the branch.
|
|
927
|
+
// Each helper is hoisted only where something emitted uses it, with what it
|
|
928
|
+
// depends on. Hoisting all of them as one block put five declarations into
|
|
929
|
+
// every module that asked about presence at all, about a sixth of a small
|
|
930
|
+
// schema's compiled module.
|
|
931
|
+
const OWN_HELPER_CODE = {
|
|
932
|
+
_PN: 'const _PN=({}).__proto__===Object.prototype;const _OP=Object.prototype;const _gp=Object.getPrototypeOf;',
|
|
933
|
+
_hop: 'const _hop=Object.prototype.hasOwnProperty;',
|
|
934
|
+
_h: 'function _h(o,k){return _hop.call(o,k)}',
|
|
935
|
+
_ok: 'function _ok(o,k){return (_PN?o.__proto__:_gp(o))===_OP||_hop.call(o,k)}',
|
|
936
|
+
_hall: 'function _hall(o,ks){for(let i=0;i<ks.length;i++)if(!_hop.call(o,ks[i]))return false;return true}',
|
|
937
|
+
}
|
|
938
|
+
const OWN_HELPER_DEPS = { _h: ['_hop'], _ok: ['_PN', '_hop'], _hall: ['_hop'] }
|
|
939
|
+
function useOwnHelper (ctx, name) {
|
|
940
|
+
for (const dep of OWN_HELPER_DEPS[name] || []) useOwnHelper(ctx, dep)
|
|
941
|
+
hoistOnce(ctx, '_own' + name, OWN_HELPER_CODE[name])
|
|
942
|
+
}
|
|
924
943
|
//
|
|
925
944
|
// Better still, the prototype is read once per object: a node that asks about
|
|
926
945
|
// presence declares `_pkN`, whether its value is an ordinary object, before
|
|
@@ -928,20 +947,26 @@ const OWN_HELPERS = 'const _hop=Object.prototype.hasOwnProperty;function _h(o,k)
|
|
|
928
947
|
// helper reading `__proto__` for each key was megamorphic, since it saw every
|
|
929
948
|
// object shape in the document, and cost the error path about a microsecond
|
|
930
949
|
// on a product document.
|
|
950
|
+
// Whether `v`, already known to be an object, has Object.prototype as its
|
|
951
|
+
// prototype, read without depending on the __proto__ accessor.
|
|
952
|
+
function protoIs (ctx, v) {
|
|
953
|
+
useOwnHelper(ctx, '_PN')
|
|
954
|
+
return `((_PN?${v}.__proto__:_gp(${v}))===_OP)`
|
|
955
|
+
}
|
|
931
956
|
function ownKeyExpr (ctx, v, key) {
|
|
932
|
-
hoistOnce(ctx, '_ownHoisted', OWN_HELPERS)
|
|
933
957
|
const k = JSON.stringify(key)
|
|
934
|
-
if (PROTO_NAMES.has(key)) return `_h(${v},${k})`
|
|
935
958
|
const pk = ctx.plainOf ? ctx.plainOf.get(v) : undefined
|
|
959
|
+
useOwnHelper(ctx, PROTO_NAMES.has(key) || pk !== undefined ? '_h' : '_ok')
|
|
960
|
+
if (PROTO_NAMES.has(key)) return `_h(${v},${k})`
|
|
936
961
|
return pk !== undefined ? `(${pk}?${k} in ${v}:_h(${v},${k}))` : `(${k} in ${v}&&_ok(${v},${k}))`
|
|
937
962
|
}
|
|
938
963
|
// For a value already read as `v[key]` and found not to be undefined: whether
|
|
939
964
|
// it came from `v` itself. `in` is known to hold, so only the second half runs.
|
|
940
965
|
function ownGuard (ctx, v, key) {
|
|
941
|
-
hoistOnce(ctx, '_ownHoisted', OWN_HELPERS)
|
|
942
966
|
const k = JSON.stringify(key)
|
|
943
|
-
if (PROTO_NAMES.has(key)) return `_h(${v},${k})`
|
|
944
967
|
const pk = ctx.plainOf ? ctx.plainOf.get(v) : undefined
|
|
968
|
+
useOwnHelper(ctx, PROTO_NAMES.has(key) || pk !== undefined ? '_h' : '_ok')
|
|
969
|
+
if (PROTO_NAMES.has(key)) return `_h(${v},${k})`
|
|
945
970
|
return pk !== undefined ? `(${pk}||_h(${v},${k}))` : `_ok(${v},${k})`
|
|
946
971
|
}
|
|
947
972
|
|
|
@@ -957,7 +982,8 @@ function withPlain (schema, v, lines, ctx, run) {
|
|
|
957
982
|
if (!ctx.plainOf) ctx.plainOf = new Map()
|
|
958
983
|
const name = '_pk' + ctx.varCounter++
|
|
959
984
|
const at = lines.length
|
|
960
|
-
|
|
985
|
+
const decl = `const ${name}=typeof ${v}==='object'&&${v}!==null&&${protoIs(ctx, v)}`
|
|
986
|
+
lines.push(decl)
|
|
961
987
|
const prev = ctx.plainOf.get(v)
|
|
962
988
|
ctx.plainOf.set(v, name)
|
|
963
989
|
try {
|
|
@@ -970,14 +996,15 @@ function withPlain (schema, v, lines, ctx, run) {
|
|
|
970
996
|
const re = new RegExp('\\b' + name + '\\b')
|
|
971
997
|
let used = false
|
|
972
998
|
for (let i = at + 1; i < lines.length && !used; i++) if (re.test(lines[i])) used = true
|
|
973
|
-
if (!used && lines[at] ===
|
|
999
|
+
if (!used && lines[at] === decl) lines.splice(at, 1)
|
|
974
1000
|
}
|
|
975
1001
|
}
|
|
976
1002
|
// The same test at run time, for the closure compiler.
|
|
977
1003
|
const _hop = Object.prototype.hasOwnProperty
|
|
1004
|
+
const _PN = ({}).__proto__ === Object.prototype
|
|
978
1005
|
function hasOwnKey (d, key) {
|
|
979
1006
|
if (PROTO_NAMES.has(key)) return _hop.call(d, key)
|
|
980
|
-
return key in d && (d.__proto__ === Object.prototype || _hop.call(d, key))
|
|
1007
|
+
return key in d && ((_PN ? d.__proto__ : Object.getPrototypeOf(d)) === Object.prototype || _hop.call(d, key))
|
|
981
1008
|
}
|
|
982
1009
|
|
|
983
1010
|
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'toString', 'valueOf',
|
|
@@ -1751,7 +1778,11 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
1751
1778
|
const defSets = ctx.defFns ? Array.from(ctx.defFns.values()) : []
|
|
1752
1779
|
const needsGuard = ctx.usesRecursion || defSets.length > 0
|
|
1753
1780
|
|
|
1754
|
-
|
|
1781
|
+
// The hybrid and tail forms are the same checks with different returns. They
|
|
1782
|
+
// are built when first asked for, not here: most validators never call
|
|
1783
|
+
// validateJSON or get extended, and rewriting and compiling the source twice
|
|
1784
|
+
// more on every schema was about a third of what a first call cost.
|
|
1785
|
+
let body, hybridThunk, tailThunk
|
|
1755
1786
|
if (ctx.usesRecursion) {
|
|
1756
1787
|
// Self-recursive: a named body reached through the cycle guard, so data
|
|
1757
1788
|
// that points back at itself settles instead of exhausting the stack.
|
|
@@ -1760,23 +1791,25 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
1760
1791
|
const run = emitGuardedRun('return _validate(d)', '', defSets)
|
|
1761
1792
|
body = `${decl}function _run(d){\n ${run}\n }\n return _run(d)`
|
|
1762
1793
|
// Hybrid: keep the boolean answer, wrap only the outer call
|
|
1763
|
-
|
|
1764
|
-
|
|
1794
|
+
hybridThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
|
|
1795
|
+
tailThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)&&__ataTail(d)`
|
|
1765
1796
|
} else if (defSets.length > 0) {
|
|
1766
1797
|
// No self-reference at the root, but a def on a cycle can still be
|
|
1767
1798
|
// re-entered with the same value; the guarded functions need their state.
|
|
1768
1799
|
const run = emitGuardedRun(`return _body(d)`, '', defSets)
|
|
1769
1800
|
body = `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)`
|
|
1770
|
-
|
|
1771
|
-
|
|
1801
|
+
hybridThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
|
|
1802
|
+
tailThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)&&__ataTail(d)`
|
|
1772
1803
|
} else {
|
|
1773
1804
|
body = checkStr + '\n return true'
|
|
1774
|
-
|
|
1805
|
+
hybridThunk = () => replaceTopLevel(checkStr + '\n return R')
|
|
1775
1806
|
// An early accept inside the checks has to reach the tail too, so every
|
|
1776
1807
|
// top-level `return true` becomes the tail call, by the same rewrite the
|
|
1777
1808
|
// hybrid above relies on and test_hybrid_agreement holds to the suite.
|
|
1778
|
-
|
|
1809
|
+
tailThunk = () => replaceTopLevel(checkStr + '\n return true', 'return __ataTail(d)', 'return false')
|
|
1779
1810
|
}
|
|
1811
|
+
let hybridBodyMemo
|
|
1812
|
+
const hybridBody = () => (hybridBodyMemo === undefined ? (hybridBodyMemo = hybridThunk()) : hybridBodyMemo)
|
|
1780
1813
|
|
|
1781
1814
|
const guardStr = needsGuard ? emitGuardState() : ''
|
|
1782
1815
|
const preambleStr = guardStr +
|
|
@@ -1794,11 +1827,20 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
1794
1827
|
boolFn = new Function('d', body)
|
|
1795
1828
|
}
|
|
1796
1829
|
|
|
1797
|
-
//
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1830
|
+
// The hybrid: same body, return R instead of true, return E(d) instead of
|
|
1831
|
+
// false. Compiled on first request; null when that compile is refused, and
|
|
1832
|
+
// the caller then assembles from the error functions instead.
|
|
1833
|
+
let hybridFactory
|
|
1834
|
+
boolFn._hybridFactory = (R, E) => {
|
|
1835
|
+
if (hybridFactory === undefined) {
|
|
1836
|
+
try {
|
|
1837
|
+
hybridFactory = new Function(...closureNames, 'R', 'E', `${preambleStr}return function(d){${hybridBody()}}`)
|
|
1838
|
+
} catch {
|
|
1839
|
+
hybridFactory = null
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
return hybridFactory === null ? null : hybridFactory(...closureValues, R, E)
|
|
1843
|
+
}
|
|
1802
1844
|
|
|
1803
1845
|
// The same function with one more check where it would return true, for a
|
|
1804
1846
|
// wrapper that enforces something the schema does not carry (see
|
|
@@ -1812,7 +1854,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
1812
1854
|
boolFn._withTail = (tail) => {
|
|
1813
1855
|
if (tailFactory === undefined) {
|
|
1814
1856
|
try {
|
|
1815
|
-
tailFactory = new Function(...closureNames, '__ataTail', `${preambleStr}return function(d){${
|
|
1857
|
+
tailFactory = new Function(...closureNames, '__ataTail', `${preambleStr}return function(d){${tailThunk()}}`)
|
|
1816
1858
|
} catch {
|
|
1817
1859
|
tailFactory = null
|
|
1818
1860
|
}
|
|
@@ -1833,7 +1875,11 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
1833
1875
|
boolFn._preambleGuard = guardStr
|
|
1834
1876
|
boolFn._preambleParts = ctx.preamble ? ctx.preamble.slice() : []
|
|
1835
1877
|
boolFn._sharedHelpers = ctx.shared ? ctx.shared.slice() : []
|
|
1836
|
-
boolFn
|
|
1878
|
+
Object.defineProperty(boolFn, '_hybridSource', {
|
|
1879
|
+
get: () => helperStr + hybridBody(),
|
|
1880
|
+
configurable: true,
|
|
1881
|
+
enumerable: false,
|
|
1882
|
+
})
|
|
1837
1883
|
boolFn._usesSafeRe = !!ctx.usesSafeRe
|
|
1838
1884
|
// Custom-format closure entries that the bundle output needs to recreate.
|
|
1839
1885
|
// Stored as { name, fn } so consumers can serialize via Function#toString.
|
|
@@ -2266,11 +2312,11 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
2266
2312
|
}
|
|
2267
2313
|
// On an object with another prototype the typed ones may be inherited.
|
|
2268
2314
|
if (typed.length > 0) {
|
|
2269
|
-
|
|
2315
|
+
useOwnHelper(ctx, '_hall')
|
|
2270
2316
|
// Read directly: the object type is already established here, and a
|
|
2271
2317
|
// flag declared for this alone costs more than it saves. The list is
|
|
2272
2318
|
// built only on that path, which an ordinary object never takes.
|
|
2273
|
-
lines.push(`if(
|
|
2319
|
+
lines.push(`if(!${protoIs(ctx, v)}&&!_hall(${v},${JSON.stringify(typed)}))return false`)
|
|
2274
2320
|
}
|
|
2275
2321
|
} else if (schema.required && schema.required.length > 0) {
|
|
2276
2322
|
const checks = schema.required.map(key => `!${ownKeyExpr(ctx, v, key)}`)
|
|
@@ -4906,6 +4952,21 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4906
4952
|
const allowed = propKeys.map(k => JSON.stringify(k)).join(',')
|
|
4907
4953
|
body = `const _a${ci}=new Set([${allowed}]);let _x${ci}=0;for(const _f${ci} in ${v}){if(!_a${ci}.has(_f${ci})){_x${ci}=1;break}}if(_x${ci}){${detail(`!_a${ci}.has(_k${ci}[_i])`)}}`
|
|
4908
4954
|
}
|
|
4955
|
+
// When every declared property is required, a clean object has exactly the
|
|
4956
|
+
// declared keys: if the enumerated count equals the declared count and
|
|
4957
|
+
// every declared name is there, the enumerated set is the declared set and
|
|
4958
|
+
// nothing is extra. That is the verdict generator's count, and it skips
|
|
4959
|
+
// comparing each key against every name, which kept this function over
|
|
4960
|
+
// twice the verdict's cost on an accepted document. Anything else, a
|
|
4961
|
+
// different count or a missing name, falls through to the scan, which
|
|
4962
|
+
// names the extra keys. A name Object.prototype carries answers `in`
|
|
4963
|
+
// through the prototype without being enumerated, so those stay on the
|
|
4964
|
+
// scan.
|
|
4965
|
+
const req = new Set(Array.isArray(schema.required) ? schema.required : [])
|
|
4966
|
+
if (propKeys.length > 0 && propKeys.every((k) => req.has(k) && !PROTO_NAMES.has(k))) {
|
|
4967
|
+
const present = propKeys.map((k) => `${JSON.stringify(k)} in ${v}`).join('&&')
|
|
4968
|
+
body = `let _n${ci}=0;for(const _f${ci} in ${v})_n${ci}++;if(_n${ci}!==${propKeys.length}||!(${present})){${body}}`
|
|
4969
|
+
}
|
|
4909
4970
|
lines.push(isObj
|
|
4910
4971
|
? `{${body}}`
|
|
4911
4972
|
: `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body}}`)
|
package/lib/tier0.js
CHANGED
|
@@ -46,6 +46,8 @@ const F_MULT = 16;
|
|
|
46
46
|
// itself; see ownKeyExpr in js-compiler.js.
|
|
47
47
|
const PROTO_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '__proto__']);
|
|
48
48
|
const _hop = Object.prototype.hasOwnProperty;
|
|
49
|
+
// Deno removes the __proto__ accessor; read the prototype another way there.
|
|
50
|
+
const _PN = ({}).__proto__ === Object.prototype;
|
|
49
51
|
|
|
50
52
|
function primConstraint(key, propSchema) {
|
|
51
53
|
const t = propSchema.type;
|
|
@@ -169,7 +171,7 @@ function tier0ValidateObject(plan, data) {
|
|
|
169
171
|
let v = data[c.key];
|
|
170
172
|
// Presence is an own-property question: a value the object inherits is
|
|
171
173
|
// treated as absent, as the interpreted engine treats it.
|
|
172
|
-
if (v !== undefined && (c.protoName || data.__proto__ !== Object.prototype) && !_hop.call(data, c.key)) v = undefined;
|
|
174
|
+
if (v !== undefined && (c.protoName || (_PN ? data.__proto__ : Object.getPrototypeOf(data)) !== Object.prototype) && !_hop.call(data, c.key)) v = undefined;
|
|
173
175
|
if (v === undefined) {
|
|
174
176
|
if (reqMask & (1 << i)) return false;
|
|
175
177
|
continue;
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.32.
|
|
3
|
+
"version": "1.32.2",
|
|
4
4
|
"description": "JSON Schema validation that compiles for speed and still runs where code generation is blocked. Compiled and interpreted engines answer identically at 100% of the official suite. TypeScript inference, Standard Schema V1, and a build step that emits dependency-free modules.",
|
|
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_scanner_differential.js && node tests/test_validate_json_scanner_cost.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_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_retry_message.js && node tests/test_describe_schema.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_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_uri_fast_path.js && node tests/test_extend_verdict.js && node tests/test_ipv4_regex.js && node tests/test_ipv6_fast_path.js && node tests/test_nested_recursion_decline.js && node tests/test_generator_error_content.js && node tests/test_inherited_key_presence.js && node tests/test_plan_source.js && node tests/test_fused_remove_additional.js && node tests/test_runtime_parse.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.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_deferred_def_scope.js && node tests/test_node_scoped_ctx_flags.js && node tests/test_fast_slot_exhaustion.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.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_unevaluated_error_shape.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_additional_props_combined_cost.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_single_pass_errors.js && node tests/test_lazy_json_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_defaults_own_keys.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_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.js && node tests/test_schema_hash.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_scanner_differential.js && node tests/test_validate_json_scanner_cost.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_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_retry_message.js && node tests/test_describe_schema.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_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_uri_fast_path.js && node tests/test_extend_verdict.js && node tests/test_ipv4_regex.js && node tests/test_ipv6_fast_path.js && node tests/test_nested_recursion_decline.js && node tests/test_generator_error_content.js && node tests/test_inherited_key_presence.js && node tests/test_proto_accessor.js && node tests/test_hybrid_tier.js && node tests/test_plan_source.js && node tests/test_fused_remove_additional.js && node tests/test_runtime_parse.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.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_deferred_def_scope.js && node tests/test_node_scoped_ctx_flags.js && node tests/test_fast_slot_exhaustion.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.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_unevaluated_error_shape.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_additional_props_combined_cost.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_single_pass_errors.js && node tests/test_lazy_json_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_defaults_own_keys.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_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.js && node tests/test_schema_hash.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",
|
|
@@ -121,13 +121,13 @@
|
|
|
121
121
|
"LICENSE"
|
|
122
122
|
],
|
|
123
123
|
"optionalDependencies": {
|
|
124
|
-
"@ata-validator/native-darwin-arm64": "1.32.
|
|
125
|
-
"@ata-validator/native-darwin-x64": "1.32.
|
|
126
|
-
"@ata-validator/native-linux-arm64-gnu": "1.32.
|
|
127
|
-
"@ata-validator/native-linux-arm64-musl": "1.32.
|
|
128
|
-
"@ata-validator/native-linux-x64-gnu": "1.32.
|
|
129
|
-
"@ata-validator/native-linux-x64-musl": "1.32.
|
|
130
|
-
"@ata-validator/native-win32-x64": "1.32.
|
|
124
|
+
"@ata-validator/native-darwin-arm64": "1.32.2",
|
|
125
|
+
"@ata-validator/native-darwin-x64": "1.32.2",
|
|
126
|
+
"@ata-validator/native-linux-arm64-gnu": "1.32.2",
|
|
127
|
+
"@ata-validator/native-linux-arm64-musl": "1.32.2",
|
|
128
|
+
"@ata-validator/native-linux-x64-gnu": "1.32.2",
|
|
129
|
+
"@ata-validator/native-linux-x64-musl": "1.32.2",
|
|
130
|
+
"@ata-validator/native-win32-x64": "1.32.2"
|
|
131
131
|
},
|
|
132
132
|
"peerDependencies": {
|
|
133
133
|
"yaml": "^2.0.0"
|