ata-validator 0.4.16 → 0.5.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 +39 -24
- package/index.js +8 -4
- package/lib/js-compiler.js +625 -40
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/ata-validator.node +0 -0
package/README.md
CHANGED
|
@@ -6,23 +6,23 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
6
6
|
|
|
7
7
|
## Performance
|
|
8
8
|
|
|
9
|
-
### Simple Schema (
|
|
9
|
+
### Simple Schema (7 properties, type + format + range + nested object)
|
|
10
10
|
|
|
11
11
|
| Scenario | ata | ajv | |
|
|
12
12
|
|---|---|---|---|
|
|
13
|
-
| **validate(obj)** valid |
|
|
14
|
-
| **validate(obj)** invalid |
|
|
15
|
-
| **isValidObject(obj)** | 28ns |
|
|
16
|
-
| **Schema compilation** |
|
|
17
|
-
| **First validation** |
|
|
13
|
+
| **validate(obj)** valid | 29ns | 109ns | **ata 3.8x faster** |
|
|
14
|
+
| **validate(obj)** invalid | 57ns | 191ns | **ata 3.3x faster** |
|
|
15
|
+
| **isValidObject(obj)** | 28ns | 109ns | **ata 3.9x faster** |
|
|
16
|
+
| **Schema compilation** | 665ns | 1.38ms | **ata 2,075x faster** |
|
|
17
|
+
| **First validation** | 2.52μs | 1.16ms | **ata 460x faster** |
|
|
18
18
|
|
|
19
19
|
### Complex Schema (patternProperties + dependentSchemas + propertyNames + additionalProperties)
|
|
20
20
|
|
|
21
21
|
| Scenario | ata | ajv | |
|
|
22
22
|
|---|---|---|---|
|
|
23
|
-
| **validate(obj)** valid |
|
|
24
|
-
| **validate(obj)** invalid |
|
|
25
|
-
| **isValidObject(obj)** |
|
|
23
|
+
| **validate(obj)** valid | 17ns | 116ns | **ata 6.8x faster** |
|
|
24
|
+
| **validate(obj)** invalid | 58ns | 194ns | **ata 3.3x faster** |
|
|
25
|
+
| **isValidObject(obj)** | 18ns | 119ns | **ata 6.5x faster** |
|
|
26
26
|
|
|
27
27
|
### Cross-Schema `$ref` (multi-schema with `$id` registry)
|
|
28
28
|
|
|
@@ -33,14 +33,29 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
33
33
|
|
|
34
34
|
> Measured with [mitata](https://github.com/evanwashere/mitata) on Apple M4 Pro (process-isolated). [Benchmark code](benchmark/bench_complex_mitata.mjs)
|
|
35
35
|
|
|
36
|
+
### unevaluatedProperties / unevaluatedItems
|
|
37
|
+
|
|
38
|
+
| Scenario | ata | ajv | |
|
|
39
|
+
|---|---|---|---|
|
|
40
|
+
| **Tier 1** (properties only) valid | 3.3ns | 8.7ns | **ata 2.6x faster** |
|
|
41
|
+
| **Tier 1** invalid | 3.7ns | 19.1ns | **ata 5.2x faster** |
|
|
42
|
+
| **Tier 2** (allOf) valid | 3.3ns | 9.9ns | **ata 3.0x faster** |
|
|
43
|
+
| **Tier 3** (anyOf) valid | 6.7ns | 23.2ns | **ata 3.5x faster** |
|
|
44
|
+
| **Tier 3** invalid | 7.1ns | 42.4ns | **ata 6.0x faster** |
|
|
45
|
+
| **unevaluatedItems** valid | 1.0ns | 5.5ns | **ata 5.4x faster** |
|
|
46
|
+
| **unevaluatedItems** invalid | 0.96ns | 14.2ns | **ata 14.8x faster** |
|
|
47
|
+
| **Compilation** | 375ns | 2.59ms | **ata 6,904x faster** |
|
|
48
|
+
|
|
49
|
+
Three-tier hybrid codegen: static schemas compile to zero-overhead key checks, dynamic schemas (anyOf/oneOf) use bitmask tracking with V8-inlined branch functions. [Benchmark code](benchmark/bench_unevaluated_mitata.mjs)
|
|
50
|
+
|
|
36
51
|
### vs Ecosystem (Zod, Valibot, TypeBox)
|
|
37
52
|
|
|
38
53
|
| Scenario | ata | ajv | typebox | zod | valibot |
|
|
39
54
|
|---|---|---|---|---|---|
|
|
40
|
-
| **validate (valid)** | **
|
|
41
|
-
| **validate (invalid)** | **
|
|
42
|
-
| **compilation** | **
|
|
43
|
-
| **first validation** | **
|
|
55
|
+
| **validate (valid)** | **9ns** | 39ns | 50ns | 339ns | 322ns |
|
|
56
|
+
| **validate (invalid)** | **38ns** | 107ns | 4ns | 12.0μs | 840ns |
|
|
57
|
+
| **compilation** | **556ns** | 1.24ms | 54μs | — | — |
|
|
58
|
+
| **first validation** | **2.0μs** | 1.16ms | 55μs | — | — |
|
|
44
59
|
|
|
45
60
|
> Different categories: ata/ajv/typebox are JSON Schema validators, zod/valibot are schema-builder DSLs. [Benchmark code](benchmark/bench_all_mitata.mjs)
|
|
46
61
|
|
|
@@ -67,7 +82,7 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
67
82
|
|
|
68
83
|
**Combined single-pass validator**: ata compiles schemas into a single function that validates and collects errors in one pass. Valid data returns `VALID_RESULT` with zero allocation. Invalid data collects errors inline with pre-allocated frozen error objects - no double validation, no try/catch (3.3x V8 deopt). Lazy compilation defers all work to first usage - constructor is near-zero cost.
|
|
69
84
|
|
|
70
|
-
**JS codegen**: Schemas are compiled to monolithic JS functions (like ajv). Full keyword support including `patternProperties`, `dependentSchemas`, `propertyNames`, cross-schema `$ref` with `$id` registry, and Draft 7 auto-detection. charCodeAt prefix matching replaces regex for simple patterns (4x faster). Merged key iteration loops (patternProperties + propertyNames + additionalProperties in a single `for..in`).
|
|
85
|
+
**JS codegen**: Schemas are compiled to monolithic JS functions (like ajv). Full keyword support including `patternProperties`, `dependentSchemas`, `propertyNames`, `unevaluatedProperties`, `unevaluatedItems`, cross-schema `$ref` with `$id` registry, and Draft 7 auto-detection. Three-tier hybrid approach for unevaluated keywords: compile-time resolution for static schemas, bitmask tracking for dynamic ones. charCodeAt prefix matching replaces regex for simple patterns (4x faster). Merged key iteration loops (patternProperties + propertyNames + additionalProperties in a single `for..in`).
|
|
71
86
|
|
|
72
87
|
**V8 TurboFan optimizations**: Destructuring batch reads, `undefined` checks instead of `in` operator, context-aware type guard elimination, property hoisting to local variables, tiered uniqueItems (nested loop for small arrays), inline key comparison for small property sets (no Set.has overhead).
|
|
73
88
|
|
|
@@ -75,15 +90,15 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
75
90
|
|
|
76
91
|
### JSON Schema Test Suite
|
|
77
92
|
|
|
78
|
-
**
|
|
93
|
+
**96.9%** pass rate (1109/1144) on official [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) (Draft 2020-12).
|
|
79
94
|
|
|
80
95
|
## When to use ata
|
|
81
96
|
|
|
82
|
-
- **High-throughput `validate(obj)`** -
|
|
83
|
-
- **Complex schemas** - `patternProperties`, `dependentSchemas`, `propertyNames` all inline JS codegen (
|
|
97
|
+
- **High-throughput `validate(obj)`** - 6.8x faster than ajv on complex schemas, 38x faster than zod
|
|
98
|
+
- **Complex schemas** - `patternProperties`, `dependentSchemas`, `propertyNames`, `unevaluatedProperties` all inline JS codegen (6.8x faster than ajv)
|
|
84
99
|
- **Multi-schema projects** - cross-schema `$ref` with `$id` registry, `addSchema()` API
|
|
85
100
|
- **Draft 7 migration** - auto-detects `$schema`, normalizes Draft 7 keywords transparently
|
|
86
|
-
- **Serverless / cold starts** -
|
|
101
|
+
- **Serverless / cold starts** - 6,904x faster compilation, 5,148x faster first validation
|
|
87
102
|
- **Security-sensitive apps** - RE2 regex, immune to ReDoS attacks
|
|
88
103
|
- **Batch/streaming validation** - NDJSON log processing, data pipelines (2.6x faster)
|
|
89
104
|
- **Standard Schema V1** - native support for Fastify v5, tRPC, TanStack
|
|
@@ -91,12 +106,12 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
91
106
|
|
|
92
107
|
## When to use ajv
|
|
93
108
|
|
|
94
|
-
- **100% spec compliance needed** - ajv covers more edge cases (ata:
|
|
95
|
-
- **`$dynamicRef
|
|
109
|
+
- **100% spec compliance needed** - ajv covers more edge cases (ata: 96.9%)
|
|
110
|
+
- **`$dynamicRef`** - not yet supported in ata
|
|
96
111
|
|
|
97
112
|
## Features
|
|
98
113
|
|
|
99
|
-
- **Hybrid validator**:
|
|
114
|
+
- **Hybrid validator**: 6.8x faster than ajv valid, 6.0x faster invalid on complex schemas - jsFn boolean guard for valid path (zero allocation), combined codegen with pre-allocated errors for invalid path. Schema compilation cache for repeated schemas
|
|
100
115
|
- **Cross-schema `$ref`**: `schemas` option and `addSchema()` API. Compile-time resolution with `$id` registry, zero runtime overhead
|
|
101
116
|
- **Draft 7 support**: Auto-detects `$schema` field, normalizes `dependencies`/`additionalItems`/`definitions` transparently
|
|
102
117
|
- **Multi-core**: Parallel validation across all CPU cores - 13.4M validations/sec
|
|
@@ -107,7 +122,7 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
107
122
|
- **Zero-copy paths**: Buffer and pre-padded input support - no unnecessary copies
|
|
108
123
|
- **Defaults + coercion**: `default` values, `coerceTypes`, `removeAdditional` support
|
|
109
124
|
- **C/C++ library**: Native API for non-Node.js environments
|
|
110
|
-
- **
|
|
125
|
+
- **96.9% spec compliant**: Draft 2020-12
|
|
111
126
|
|
|
112
127
|
## Installation
|
|
113
128
|
|
|
@@ -256,8 +271,8 @@ auto result = ata::validate(schema, R"({"name": "Mert"})");
|
|
|
256
271
|
| Type | `type` |
|
|
257
272
|
| Numeric | `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf` |
|
|
258
273
|
| String | `minLength`, `maxLength`, `pattern`, `format` |
|
|
259
|
-
| Array | `items`, `prefixItems`, `minItems`, `maxItems`, `uniqueItems`, `contains`, `minContains`, `maxContains` |
|
|
260
|
-
| Object | `properties`, `required`, `additionalProperties`, `patternProperties`, `minProperties`, `maxProperties`, `propertyNames`, `dependentRequired`, `dependentSchemas` |
|
|
274
|
+
| Array | `items`, `prefixItems`, `minItems`, `maxItems`, `uniqueItems`, `contains`, `minContains`, `maxContains`, `unevaluatedItems` |
|
|
275
|
+
| Object | `properties`, `required`, `additionalProperties`, `patternProperties`, `minProperties`, `maxProperties`, `propertyNames`, `dependentRequired`, `dependentSchemas`, `unevaluatedProperties` |
|
|
261
276
|
| Enum/Const | `enum`, `const` |
|
|
262
277
|
| Composition | `allOf`, `anyOf`, `oneOf`, `not` |
|
|
263
278
|
| Conditional | `if`, `then`, `else` |
|
package/index.js
CHANGED
|
@@ -443,12 +443,16 @@ class Validator {
|
|
|
443
443
|
} catch {}
|
|
444
444
|
}
|
|
445
445
|
// errFn: use JS codegen if safe, else lazy-native fallback
|
|
446
|
+
// For unevaluated schemas without errFn, use jsFn as boolean-only fallback
|
|
447
|
+
const hasUnevaluated = schemaObj && JSON.stringify(schemaObj).includes('unevaluatedProperties') || JSON.stringify(schemaObj).includes('unevaluatedItems')
|
|
446
448
|
const errFn =
|
|
447
449
|
safeErrFn ||
|
|
448
|
-
(
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
450
|
+
(hasUnevaluated
|
|
451
|
+
? (d) => ({ valid: jsFn(d), errors: jsFn(d) ? [] : [{ code: 'unevaluated', path: '', message: 'unevaluated property or item' }] })
|
|
452
|
+
: (d) => {
|
|
453
|
+
this._ensureNative();
|
|
454
|
+
return this._compiled.validate(d);
|
|
455
|
+
});
|
|
452
456
|
|
|
453
457
|
// Best path: combined validator (single pass, validates + collects errors)
|
|
454
458
|
// Valid data: returns VALID_RESULT, no allocation
|
package/lib/js-compiler.js
CHANGED
|
@@ -433,11 +433,13 @@ function codegenSafe(schema, schemaMap) {
|
|
|
433
433
|
if (typeof schema !== 'object' || schema === null) return true
|
|
434
434
|
|
|
435
435
|
// Boolean sub-schemas anywhere cause bail — codegen doesn't handle schema=false correctly
|
|
436
|
-
if (schema.items === false
|
|
436
|
+
if (schema.items === false) return false
|
|
437
|
+
if (schema.items === true && !schema.unevaluatedItems) return false
|
|
437
438
|
if (schema.additionalProperties === true) return true // permissive — fine
|
|
438
439
|
if (schema.properties) {
|
|
439
440
|
for (const v of Object.values(schema.properties)) {
|
|
440
|
-
if (
|
|
441
|
+
if (v === false) return false // property: false is complex
|
|
442
|
+
if (v === true) continue // property: true is always valid
|
|
441
443
|
if (!codegenSafe(v, schemaMap)) return false
|
|
442
444
|
}
|
|
443
445
|
}
|
|
@@ -463,9 +465,10 @@ function codegenSafe(schema, schemaMap) {
|
|
|
463
465
|
const isLocal = /^#\/(?:\$defs|definitions)\/[^/]+$/.test(schema.$ref)
|
|
464
466
|
const isResolvable = !isLocal && schemaMap && schemaMap.has(schema.$ref)
|
|
465
467
|
if (!isLocal && !isResolvable) return false
|
|
466
|
-
//
|
|
468
|
+
// In Draft 2020-12, $ref with siblings is allowed. Only bail if no unevaluated* keyword
|
|
469
|
+
// (unevaluated schemas need $ref + siblings to work properly)
|
|
467
470
|
const siblings = Object.keys(schema).filter(k => k !== '$ref' && k !== '$defs' && k !== 'definitions' && k !== '$schema' && k !== '$id')
|
|
468
|
-
if (siblings.length > 0) return false
|
|
471
|
+
if (siblings.length > 0 && schema.unevaluatedProperties === undefined && schema.unevaluatedItems === undefined) return false
|
|
469
472
|
}
|
|
470
473
|
|
|
471
474
|
// additionalProperties as schema — bail entirely, too many edge cases with allOf interaction
|
|
@@ -475,6 +478,19 @@ function codegenSafe(schema, schemaMap) {
|
|
|
475
478
|
// propertyNames: false — codegen doesn't handle this
|
|
476
479
|
if (schema.propertyNames === false) return false
|
|
477
480
|
|
|
481
|
+
// unevaluatedProperties: allow boolean and schema values
|
|
482
|
+
if (schema.unevaluatedProperties !== undefined) {
|
|
483
|
+
if (typeof schema.unevaluatedProperties === 'object' && schema.unevaluatedProperties !== null) {
|
|
484
|
+
if (!codegenSafe(schema.unevaluatedProperties, schemaMap)) return false
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// unevaluatedItems: allow boolean and schema values
|
|
488
|
+
if (schema.unevaluatedItems !== undefined) {
|
|
489
|
+
if (typeof schema.unevaluatedItems === 'object' && schema.unevaluatedItems !== null) {
|
|
490
|
+
if (!codegenSafe(schema.unevaluatedItems, schemaMap)) return false
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
478
494
|
// Check $defs: targets must be safe, names must be simple, no nested $ref chains
|
|
479
495
|
const defs = schema.$defs || schema.definitions
|
|
480
496
|
if (defs) {
|
|
@@ -501,7 +517,8 @@ function codegenSafe(schema, schemaMap) {
|
|
|
501
517
|
if (typeof schema.additionalProperties === 'object') subs.push(schema.additionalProperties)
|
|
502
518
|
for (const s of subs) {
|
|
503
519
|
if (s === undefined || s === null) continue
|
|
504
|
-
if (
|
|
520
|
+
if (s === false) return false // boolean false sub-schema — complex
|
|
521
|
+
if (s === true) continue // boolean true sub-schema — always valid, fine
|
|
505
522
|
if (!codegenSafe(s, schemaMap)) return false
|
|
506
523
|
}
|
|
507
524
|
|
|
@@ -548,13 +565,14 @@ function compileToJSCodegen(schema, schemaMap) {
|
|
|
548
565
|
const ctx = { varCounter: 0, helpers: [], helperCode: [], closureVars: [], closureVals: [], rootDefs, refStack: new Set(), schemaMap: schemaMap || null }
|
|
549
566
|
const lines = []
|
|
550
567
|
genCode(schema, 'd', lines, ctx)
|
|
551
|
-
if (lines.length === 0) return () => true
|
|
552
568
|
|
|
553
|
-
// Append deferred checks (additionalProperties) at the end
|
|
569
|
+
// Append deferred checks (additionalProperties, unevaluatedProperties) at the end
|
|
554
570
|
if (ctx.deferredChecks) {
|
|
555
571
|
for (const dc of ctx.deferredChecks) lines.push(dc)
|
|
556
572
|
}
|
|
557
573
|
|
|
574
|
+
if (lines.length === 0) return () => true
|
|
575
|
+
|
|
558
576
|
const checkStr = lines.join('\n ')
|
|
559
577
|
|
|
560
578
|
// Regex and helpers are passed as closure variables (not re-created per call)
|
|
@@ -641,25 +659,32 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
641
659
|
if (typeof schema !== 'object' || schema === null) return
|
|
642
660
|
|
|
643
661
|
// $ref — guard against circular references
|
|
662
|
+
// In 2020-12 with unevaluated*, $ref can coexist with siblings — don't early return
|
|
663
|
+
// Only when THIS schema has unevaluated keywords directly (not via $ref target)
|
|
664
|
+
const hasSiblings = schema.$ref && (schema.unevaluatedProperties !== undefined || schema.unevaluatedItems !== undefined)
|
|
644
665
|
if (schema.$ref) {
|
|
645
666
|
// 1. Local ref
|
|
646
667
|
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
647
668
|
if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
|
|
648
|
-
if (ctx.refStack.has(schema.$ref)) return
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
|
|
656
|
-
|
|
657
|
-
ctx.refStack.
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
669
|
+
if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
|
|
670
|
+
else {
|
|
671
|
+
ctx.refStack.add(schema.$ref)
|
|
672
|
+
genCode(ctx.rootDefs[m[1]], v, lines, ctx, knownType)
|
|
673
|
+
ctx.refStack.delete(schema.$ref)
|
|
674
|
+
if (!hasSiblings) return
|
|
675
|
+
}
|
|
676
|
+
} else if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
|
|
677
|
+
// 2. Cross-schema ref
|
|
678
|
+
if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
|
|
679
|
+
else {
|
|
680
|
+
ctx.refStack.add(schema.$ref)
|
|
681
|
+
genCode(ctx.schemaMap.get(schema.$ref), v, lines, ctx, knownType)
|
|
682
|
+
ctx.refStack.delete(schema.$ref)
|
|
683
|
+
if (!hasSiblings) return
|
|
684
|
+
}
|
|
685
|
+
} else {
|
|
686
|
+
if (!hasSiblings) return
|
|
661
687
|
}
|
|
662
|
-
return
|
|
663
688
|
}
|
|
664
689
|
|
|
665
690
|
// Determine the single known type after this schema's type check
|
|
@@ -744,6 +769,27 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
744
769
|
}
|
|
745
770
|
}
|
|
746
771
|
|
|
772
|
+
// Early key count for unevaluatedProperties: false (before properties, 10% faster)
|
|
773
|
+
// V8 branch prediction benefits from for-in iteration before property access
|
|
774
|
+
if (schema.unevaluatedProperties === false && schema.properties && schema.required && isObj) {
|
|
775
|
+
const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
776
|
+
if (!evalResult.dynamic && !evalResult.allProps) {
|
|
777
|
+
const knownKeys = evalResult.props
|
|
778
|
+
const propCount = knownKeys.length
|
|
779
|
+
const allRequired = schema.required.length >= propCount &&
|
|
780
|
+
knownKeys.every(k => schema.required.includes(k))
|
|
781
|
+
if (allRequired && propCount > 0) {
|
|
782
|
+
// Adaptive: for-in for <=15 keys (V8 fast path), Object.keys for >15
|
|
783
|
+
if (propCount <= 15) {
|
|
784
|
+
lines.push(`var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`)
|
|
785
|
+
} else {
|
|
786
|
+
lines.push(`if(Object.keys(${v}).length!==${propCount})return false`)
|
|
787
|
+
}
|
|
788
|
+
ctx._earlyKeyCount = true // flag to skip deferred check
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
747
793
|
// numeric — skip type guard if known numeric
|
|
748
794
|
if (schema.minimum !== undefined) lines.push(isNum ? `if(${v}<${schema.minimum})return false` : `if(typeof ${v}==='number'&&${v}<${schema.minimum})return false`)
|
|
749
795
|
if (schema.maximum !== undefined) lines.push(isNum ? `if(${v}>${schema.maximum})return false` : `if(typeof ${v}==='number'&&${v}>${schema.maximum})return false`)
|
|
@@ -798,7 +844,9 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
798
844
|
const propCount = Object.keys(schema.properties).length
|
|
799
845
|
const allRequired = schema.required && schema.required.length === propCount
|
|
800
846
|
const inner = allRequired
|
|
801
|
-
?
|
|
847
|
+
? (propCount <= 15
|
|
848
|
+
? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
|
|
849
|
+
: `if(Object.keys(${v}).length!==${propCount})return false`)
|
|
802
850
|
: `for(var _k in ${v})if(${Object.keys(schema.properties).map(k => `_k!==${JSON.stringify(k)}`).join('&&')})return false`
|
|
803
851
|
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
804
852
|
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
@@ -852,16 +900,6 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
852
900
|
ctx._ppHandledAdditional = true
|
|
853
901
|
ctx._ppHandledPropertyNames = !!pn
|
|
854
902
|
const propKeys = Object.keys(schema.properties)
|
|
855
|
-
// Inline key comparison instead of Set.has for small property counts (faster, no allocation)
|
|
856
|
-
const keyCheck = propKeys.length <= 8
|
|
857
|
-
? propKeys.map(k => `${kVar}===${JSON.stringify(k)}`).join('||')
|
|
858
|
-
: null
|
|
859
|
-
if (!keyCheck) {
|
|
860
|
-
const allowedSet = `_as${pi}`
|
|
861
|
-
ctx.closureVars.push(allowedSet)
|
|
862
|
-
ctx.closureVals.push(new Set(propKeys))
|
|
863
|
-
}
|
|
864
|
-
|
|
865
903
|
lines.push(`${guard}{for(const ${kVar} in ${v}){`)
|
|
866
904
|
// propertyNames checks (merged into same loop)
|
|
867
905
|
if (pn) {
|
|
@@ -886,14 +924,21 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
886
924
|
lines.push(`if(!_es${ei}.has(${kVar}))return false`)
|
|
887
925
|
}
|
|
888
926
|
}
|
|
889
|
-
// Check: is key
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
927
|
+
// Check: is key declared or matches a pattern?
|
|
928
|
+
// switch/case: V8 compiles string cases to jump table (faster than chained ===)
|
|
929
|
+
const switchCases = propKeys.map(k => `case ${JSON.stringify(k)}:`).join('')
|
|
930
|
+
lines.push(`switch(${kVar}){${switchCases}break;default:`)
|
|
931
|
+
// Default: key is not declared — must match a pattern
|
|
932
|
+
let patternChecks = []
|
|
893
933
|
for (let i = 0; i < ppEntries.length; i++) {
|
|
894
|
-
|
|
934
|
+
patternChecks.push(`if(${matchers[i].check}){if(!_ppf${pi}_${i}(${v}[${kVar}]))return false}else{return false}`)
|
|
935
|
+
}
|
|
936
|
+
if (patternChecks.length > 0) {
|
|
937
|
+
lines.push(patternChecks.join(''))
|
|
938
|
+
} else {
|
|
939
|
+
lines.push(`return false`)
|
|
895
940
|
}
|
|
896
|
-
lines.push(`
|
|
941
|
+
lines.push(`}`) // end switch
|
|
897
942
|
lines.push(`}}`)
|
|
898
943
|
} else {
|
|
899
944
|
// No additionalProperties: validate matching keys + propertyNames
|
|
@@ -1040,7 +1085,8 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1040
1085
|
}
|
|
1041
1086
|
|
|
1042
1087
|
// anyOf — need function wrappers since genCode uses return false
|
|
1043
|
-
if (
|
|
1088
|
+
// Skip standard anyOf if unevaluatedProperties will handle it (single-pass optimization)
|
|
1089
|
+
if (schema.anyOf && schema.unevaluatedProperties === undefined) {
|
|
1044
1090
|
const fns = []
|
|
1045
1091
|
for (let i = 0; i < schema.anyOf.length; i++) {
|
|
1046
1092
|
const subLines = []
|
|
@@ -1110,6 +1156,389 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
1110
1156
|
lines.push(`{const _if${fi}=${ifFn};const _th${fi}=${thenFn};const _el${fi}=${elseFn}`)
|
|
1111
1157
|
lines.push(`if(_if${fi}(${v})){if(_th${fi}&&!_th${fi}(${v}))return false}else{if(_el${fi}&&!_el${fi}(${v}))return false}}`)
|
|
1112
1158
|
}
|
|
1159
|
+
|
|
1160
|
+
// unevaluatedProperties
|
|
1161
|
+
if (schema.unevaluatedProperties !== undefined) {
|
|
1162
|
+
const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
1163
|
+
|
|
1164
|
+
if (evalResult.allProps || schema.unevaluatedProperties === true) {
|
|
1165
|
+
// All props evaluated or unevaluatedProperties:true — no-op
|
|
1166
|
+
} else if (!evalResult.dynamic) {
|
|
1167
|
+
// Tier 1-2: all evaluated props known at compile-time — ZERO COST
|
|
1168
|
+
const knownKeys = evalResult.props
|
|
1169
|
+
const propCount = knownKeys.length
|
|
1170
|
+
|
|
1171
|
+
if (schema.unevaluatedProperties === false) {
|
|
1172
|
+
const allRequired = schema.required && schema.required.length >= propCount &&
|
|
1173
|
+
knownKeys.every(k => schema.required.includes(k))
|
|
1174
|
+
|
|
1175
|
+
let inner
|
|
1176
|
+
if (allRequired && propCount > 0) {
|
|
1177
|
+
// TRICK 1: required covers all — key count check only
|
|
1178
|
+
if (!ctx._earlyKeyCount) {
|
|
1179
|
+
// Adaptive: for-in for <=15 keys, Object.keys for >15
|
|
1180
|
+
inner = propCount <= 15
|
|
1181
|
+
? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
|
|
1182
|
+
: `if(Object.keys(${v}).length!==${propCount})return false`
|
|
1183
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1184
|
+
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1185
|
+
}
|
|
1186
|
+
// else: already emitted early (before properties)
|
|
1187
|
+
} else if (propCount > 0) {
|
|
1188
|
+
// TRICK 3: charCodeAt switch tree
|
|
1189
|
+
inner = genCharCodeSwitch(knownKeys, v)
|
|
1190
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1191
|
+
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1192
|
+
} else {
|
|
1193
|
+
inner = `for(var _k in ${v})return false`
|
|
1194
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1195
|
+
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1196
|
+
}
|
|
1197
|
+
} else if (typeof schema.unevaluatedProperties === 'object') {
|
|
1198
|
+
// unevaluatedProperties: {schema} — validate unknown keys
|
|
1199
|
+
const ui = ctx.varCounter++
|
|
1200
|
+
const ukVar = `_uk${ui}`
|
|
1201
|
+
const subLines = []
|
|
1202
|
+
genCode(schema.unevaluatedProperties, `${v}[${ukVar}]`, subLines, ctx)
|
|
1203
|
+
if (subLines.length > 0) {
|
|
1204
|
+
const check = subLines.join(';')
|
|
1205
|
+
const keyChecks = knownKeys.map(k => `${ukVar}===${JSON.stringify(k)}`).join('||')
|
|
1206
|
+
const skipKnown = knownKeys.length > 0 ? `if(${keyChecks})continue;` : ''
|
|
1207
|
+
const inner = `for(var ${ukVar} in ${v}){${skipKnown}${check}}`
|
|
1208
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1209
|
+
ctx.deferredChecks.push(isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
} else {
|
|
1213
|
+
// Tier 2.5 / Tier 3: dynamic — runtime tracking needed
|
|
1214
|
+
// Compute base props: only unconditionally evaluated (properties, allOf-static, $ref)
|
|
1215
|
+
const baseResult = { props: [], items: null, allProps: false, allItems: false, dynamic: false }
|
|
1216
|
+
if (schema.properties) {
|
|
1217
|
+
for (const k of Object.keys(schema.properties)) {
|
|
1218
|
+
if (!baseResult.props.includes(k)) baseResult.props.push(k)
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (schema.allOf) {
|
|
1222
|
+
for (const sub of schema.allOf) {
|
|
1223
|
+
const subR = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
|
|
1224
|
+
if (!subR.dynamic && subR.props) {
|
|
1225
|
+
for (const k of subR.props) {
|
|
1226
|
+
if (!baseResult.props.includes(k)) baseResult.props.push(k)
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
const baseProps = baseResult.props
|
|
1232
|
+
const branchKeyword = schema.anyOf ? 'anyOf' : schema.oneOf ? 'oneOf' : null
|
|
1233
|
+
|
|
1234
|
+
if (schema.unevaluatedProperties === false) {
|
|
1235
|
+
if (schema.if && (schema.then || schema.else) && !branchKeyword && !schema.patternProperties && !schema.dependentSchemas) {
|
|
1236
|
+
// Tier 2.5: if/then/else — re-emit if function + branch-inline duplication
|
|
1237
|
+
// Can't reuse _if from above (block-scoped), so regenerate
|
|
1238
|
+
const ifLines2 = []
|
|
1239
|
+
genCode(schema.if, '_iv2', ifLines2, ctx)
|
|
1240
|
+
const ufi = ctx.varCounter++
|
|
1241
|
+
const ifFn2 = ifLines2.length === 0
|
|
1242
|
+
? `function(_iv2){return true}`
|
|
1243
|
+
: `function(_iv2){${ifLines2.join(';')};return true}`
|
|
1244
|
+
|
|
1245
|
+
// if props are only evaluated when if matches (spec: failed applicators produce no annotations)
|
|
1246
|
+
const ifProps = []
|
|
1247
|
+
if (schema.if && schema.if.properties) ifProps.push(...Object.keys(schema.if.properties))
|
|
1248
|
+
const thenEval = schema.then ? collectEvaluated(schema.then, ctx.schemaMap, ctx.rootDefs) : { props: [] }
|
|
1249
|
+
const elseEval = schema.else ? collectEvaluated(schema.else, ctx.schemaMap, ctx.rootDefs) : { props: [] }
|
|
1250
|
+
const uniqueThen = [...new Set([...baseProps, ...ifProps, ...(thenEval.props || [])])]
|
|
1251
|
+
const uniqueElse = [...new Set([...baseProps, ...(elseEval.props || [])])]
|
|
1252
|
+
|
|
1253
|
+
const thenCheck = genCharCodeSwitch(uniqueThen, v)
|
|
1254
|
+
const elseCheck = genCharCodeSwitch(uniqueElse, v)
|
|
1255
|
+
const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
1256
|
+
lines.push(`${guard}{const _uif${ufi}=${ifFn2};if(_uif${ufi}(${v})){${thenCheck}}else{${elseCheck}}}`)
|
|
1257
|
+
} else if (branchKeyword) {
|
|
1258
|
+
// Tier 3: anyOf/oneOf — runtime tracking
|
|
1259
|
+
const branches = schema[branchKeyword]
|
|
1260
|
+
const branchProps = []
|
|
1261
|
+
for (const sub of branches) {
|
|
1262
|
+
const subResult = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
|
|
1263
|
+
branchProps.push(subResult.props || [])
|
|
1264
|
+
}
|
|
1265
|
+
const allDynamicKeys = [...new Set(branchProps.flat())]
|
|
1266
|
+
const dynamicOnly = allDynamicKeys.filter(k => !baseProps.includes(k))
|
|
1267
|
+
|
|
1268
|
+
if (dynamicOnly.length > 0 && dynamicOnly.length <= 32) {
|
|
1269
|
+
// TRICK 5: bit-packed evaluated set — SINGLE PASS (validation + tracking combined)
|
|
1270
|
+
const ei = ctx.varCounter++
|
|
1271
|
+
const evVar = `_ev${ei}`
|
|
1272
|
+
const bitMap = new Map()
|
|
1273
|
+
dynamicOnly.forEach((k, i) => bitMap.set(k, i))
|
|
1274
|
+
const branchMasks = branchProps.map(props => {
|
|
1275
|
+
let mask = 0
|
|
1276
|
+
for (const p of props) {
|
|
1277
|
+
if (bitMap.has(p)) mask |= (1 << bitMap.get(p))
|
|
1278
|
+
}
|
|
1279
|
+
return mask
|
|
1280
|
+
})
|
|
1281
|
+
|
|
1282
|
+
// TRICK 4: Direct function calls — no array, no loop, V8 can inline
|
|
1283
|
+
const bfi = ctx.varCounter++
|
|
1284
|
+
lines.push(`{let ${evVar}=0`)
|
|
1285
|
+
const fnVars = []
|
|
1286
|
+
for (let i = 0; i < branches.length; i++) {
|
|
1287
|
+
const subLines2 = []
|
|
1288
|
+
genCode(branches[i], '_bv', subLines2, ctx)
|
|
1289
|
+
const fnVar = `_bf${bfi}_${i}`
|
|
1290
|
+
fnVars.push(fnVar)
|
|
1291
|
+
const fnBody = subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`
|
|
1292
|
+
lines.push(`const ${fnVar}=${fnBody}`)
|
|
1293
|
+
}
|
|
1294
|
+
if (branchKeyword === 'oneOf') {
|
|
1295
|
+
// oneOf: exactly one must match — direct calls
|
|
1296
|
+
lines.push(`let _oc${bfi}=0`)
|
|
1297
|
+
for (let i = 0; i < branches.length; i++) {
|
|
1298
|
+
lines.push(`if(${fnVars[i]}(${v})){_oc${bfi}++;${evVar}=${branchMasks[i]};if(_oc${bfi}>1)return false}`)
|
|
1299
|
+
}
|
|
1300
|
+
lines.push(`if(_oc${bfi}!==1)return false`)
|
|
1301
|
+
} else {
|
|
1302
|
+
// anyOf: at least one must match — direct calls, collect all
|
|
1303
|
+
lines.push(`let _am${bfi}=false`)
|
|
1304
|
+
for (let i = 0; i < branches.length; i++) {
|
|
1305
|
+
lines.push(`if(${fnVars[i]}(${v})){_am${bfi}=true;${evVar}|=${branchMasks[i]}}`)
|
|
1306
|
+
}
|
|
1307
|
+
lines.push(`if(!_am${bfi})return false`)
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Final check: static keys inline + dynamic keys via bitmask
|
|
1311
|
+
const staticCheck = baseProps.length > 0 ? baseProps.map(k => `_k===${JSON.stringify(k)}`).join('||') : ''
|
|
1312
|
+
const groups = new Map()
|
|
1313
|
+
for (const k of dynamicOnly) {
|
|
1314
|
+
const cc = k.charCodeAt(0)
|
|
1315
|
+
if (!groups.has(cc)) groups.set(cc, [])
|
|
1316
|
+
groups.get(cc).push(k)
|
|
1317
|
+
}
|
|
1318
|
+
let switchCases = ''
|
|
1319
|
+
for (const [cc, groupKeys] of groups) {
|
|
1320
|
+
const cond = groupKeys.map(k => `_k===${JSON.stringify(k)}&&(${evVar}&${1 << bitMap.get(k)})`).join('||')
|
|
1321
|
+
switchCases += `case ${cc}:if(${cond})continue;break;`
|
|
1322
|
+
}
|
|
1323
|
+
const dynamicCheck = `switch(_k.charCodeAt(0)){${switchCases}default:break}`
|
|
1324
|
+
const inner = staticCheck
|
|
1325
|
+
? `for(var _k in ${v}){if(${staticCheck})continue;${dynamicCheck}return false}`
|
|
1326
|
+
: `for(var _k in ${v}){${dynamicCheck}return false}`
|
|
1327
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1328
|
+
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1329
|
+
} else {
|
|
1330
|
+
// Fallback: plain object tracking
|
|
1331
|
+
const ei = ctx.varCounter++
|
|
1332
|
+
const evVar = `_ev${ei}`
|
|
1333
|
+
const fns = []
|
|
1334
|
+
for (let i = 0; i < branches.length; i++) {
|
|
1335
|
+
const subLines2 = []
|
|
1336
|
+
genCode(branches[i], '_bv', subLines2, ctx)
|
|
1337
|
+
fns.push(subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`)
|
|
1338
|
+
}
|
|
1339
|
+
const bfi = ctx.varCounter++
|
|
1340
|
+
ctx.closureVars.push(`_bk${bfi}`)
|
|
1341
|
+
ctx.closureVals.push(branchProps)
|
|
1342
|
+
lines.push(`{const ${evVar}={}`)
|
|
1343
|
+
for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
|
|
1344
|
+
lines.push(`const _bf${bfi}=[${fns.join(',')}]`)
|
|
1345
|
+
if (branchKeyword === 'oneOf') {
|
|
1346
|
+
// Single pass: validate oneOf (exactly one) + track evaluated
|
|
1347
|
+
lines.push(`let _oc${bfi}=0;for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){_oc${bfi}++;for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1;if(_oc${bfi}>1)return false}}if(_oc${bfi}!==1)return false`)
|
|
1348
|
+
} else {
|
|
1349
|
+
// Single pass: validate anyOf (at least one) + track all matching
|
|
1350
|
+
lines.push(`let _am${bfi}=false;for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){_am${bfi}=true;for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1}}if(!_am${bfi})return false`)
|
|
1351
|
+
}
|
|
1352
|
+
const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
|
|
1353
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1354
|
+
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1355
|
+
}
|
|
1356
|
+
} else if (schema.dependentSchemas) {
|
|
1357
|
+
// dependentSchemas: conditional merge at runtime
|
|
1358
|
+
const ei = ctx.varCounter++
|
|
1359
|
+
const evVar = `_ev${ei}`
|
|
1360
|
+
lines.push(`{const ${evVar}={}`)
|
|
1361
|
+
for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
|
|
1362
|
+
for (const [trigger, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
1363
|
+
const depResult = collectEvaluated(depSchema, ctx.schemaMap, ctx.rootDefs)
|
|
1364
|
+
if (depResult.props && depResult.props.length > 0) {
|
|
1365
|
+
lines.push(`if(${JSON.stringify(trigger)} in ${v}){${depResult.props.map(k => `${evVar}[${JSON.stringify(k)}]=1`).join(';')}}`)
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
|
|
1369
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1370
|
+
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1371
|
+
} else if (schema.patternProperties) {
|
|
1372
|
+
// patternProperties: runtime key matching
|
|
1373
|
+
const ei = ctx.varCounter++
|
|
1374
|
+
const evVar = `_ev${ei}`
|
|
1375
|
+
lines.push(`{const ${evVar}={}`)
|
|
1376
|
+
for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
|
|
1377
|
+
const patterns = Object.keys(schema.patternProperties)
|
|
1378
|
+
const reVars = []
|
|
1379
|
+
for (const pat of patterns) {
|
|
1380
|
+
const ri = ctx.varCounter++
|
|
1381
|
+
ctx.closureVars.push(`_ure${ri}`)
|
|
1382
|
+
ctx.closureVals.push(new RegExp(pat))
|
|
1383
|
+
reVars.push(`_ure${ri}`)
|
|
1384
|
+
}
|
|
1385
|
+
const inner = `for(var _k in ${v}){if(${evVar}[_k])continue;${reVars.map(rv => `if(${rv}.test(_k)){${evVar}[_k]=1;continue}`).join('')}return false}`
|
|
1386
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1387
|
+
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1388
|
+
}
|
|
1389
|
+
} else if (typeof schema.unevaluatedProperties === 'object') {
|
|
1390
|
+
// Tier 3 with schema: validate unknown keys against sub-schema
|
|
1391
|
+
const ei = ctx.varCounter++
|
|
1392
|
+
const evVar = `_ev${ei}`
|
|
1393
|
+
const ukVar = `_uk${ei}`
|
|
1394
|
+
lines.push(`{const ${evVar}={}`)
|
|
1395
|
+
for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
|
|
1396
|
+
|
|
1397
|
+
if (branchKeyword) {
|
|
1398
|
+
const branches = schema[branchKeyword]
|
|
1399
|
+
const branchProps = []
|
|
1400
|
+
for (const sub of branches) {
|
|
1401
|
+
const subResult = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
|
|
1402
|
+
branchProps.push(subResult.props || [])
|
|
1403
|
+
}
|
|
1404
|
+
const fns = []
|
|
1405
|
+
for (let i = 0; i < branches.length; i++) {
|
|
1406
|
+
const subLines2 = []
|
|
1407
|
+
genCode(branches[i], '_bv', subLines2, ctx)
|
|
1408
|
+
fns.push(subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`)
|
|
1409
|
+
}
|
|
1410
|
+
const bfi = ctx.varCounter++
|
|
1411
|
+
ctx.closureVars.push(`_bk${bfi}`)
|
|
1412
|
+
ctx.closureVals.push(branchProps)
|
|
1413
|
+
lines.push(`const _bf${bfi}=[${fns.join(',')}]`)
|
|
1414
|
+
if (branchKeyword === 'oneOf') {
|
|
1415
|
+
lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1;break}}`)
|
|
1416
|
+
} else {
|
|
1417
|
+
lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1}}`)
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
const subLines2 = []
|
|
1422
|
+
genCode(schema.unevaluatedProperties, `${v}[${ukVar}]`, subLines2, ctx)
|
|
1423
|
+
if (subLines2.length > 0) {
|
|
1424
|
+
const check = subLines2.join(';')
|
|
1425
|
+
const inner = `for(var ${ukVar} in ${v}){if(${evVar}[${ukVar}])continue;${check}}`
|
|
1426
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1427
|
+
ctx.deferredChecks.push(isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
1428
|
+
} else {
|
|
1429
|
+
lines.push('}')
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// unevaluatedItems
|
|
1436
|
+
if (schema.unevaluatedItems !== undefined) {
|
|
1437
|
+
const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
1438
|
+
|
|
1439
|
+
if (evalResult.allItems || schema.unevaluatedItems === true) {
|
|
1440
|
+
// All items evaluated or unevaluatedItems:true — no-op
|
|
1441
|
+
} else if (!evalResult.dynamic) {
|
|
1442
|
+
// Static: all evaluated items known at compile-time
|
|
1443
|
+
if (schema.unevaluatedItems === false) {
|
|
1444
|
+
// TRICK 6: Array.length comparison only
|
|
1445
|
+
const maxIdx = evalResult.items || 0
|
|
1446
|
+
const inner = `if(${v}.length>${maxIdx})return false`
|
|
1447
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1448
|
+
ctx.deferredChecks.push(isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
1449
|
+
} else if (typeof schema.unevaluatedItems === 'object') {
|
|
1450
|
+
const maxIdx = evalResult.items || 0
|
|
1451
|
+
const ui = ctx.varCounter++
|
|
1452
|
+
const elemVar = `_ue${ui}`
|
|
1453
|
+
const idxVar = `_ui${ui}`
|
|
1454
|
+
const subLines = []
|
|
1455
|
+
genCode(schema.unevaluatedItems, elemVar, subLines, ctx)
|
|
1456
|
+
if (subLines.length > 0) {
|
|
1457
|
+
const check = subLines.join(';')
|
|
1458
|
+
const inner = `for(let ${idxVar}=${maxIdx};${idxVar}<${v}.length;${idxVar}++){const ${elemVar}=${v}[${idxVar}];${check}}`
|
|
1459
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1460
|
+
ctx.deferredChecks.push(isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
} else {
|
|
1464
|
+
// Dynamic: runtime tracking of max evaluated index
|
|
1465
|
+
const baseIdx = evalResult.items || 0
|
|
1466
|
+
const branchKeyword = schema.anyOf ? 'anyOf' : schema.oneOf ? 'oneOf' : null
|
|
1467
|
+
|
|
1468
|
+
if (branchKeyword && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
|
|
1469
|
+
// anyOf/oneOf: each branch may evaluate different number of items
|
|
1470
|
+
const branches = schema[branchKeyword]
|
|
1471
|
+
const branchMaxIdx = []
|
|
1472
|
+
for (const sub of branches) {
|
|
1473
|
+
const subR = collectEvaluated(sub, ctx.schemaMap, ctx.rootDefs)
|
|
1474
|
+
branchMaxIdx.push(subR.items || 0)
|
|
1475
|
+
}
|
|
1476
|
+
// Runtime: find max evaluated index across all matching branches
|
|
1477
|
+
const fns = []
|
|
1478
|
+
for (let i = 0; i < branches.length; i++) {
|
|
1479
|
+
const subLines2 = []
|
|
1480
|
+
genCode(branches[i], '_bv', subLines2, ctx)
|
|
1481
|
+
fns.push(subLines2.length === 0 ? `function(_bv){return true}` : `function(_bv){${subLines2.join(';')};return true}`)
|
|
1482
|
+
}
|
|
1483
|
+
const bfi = ctx.varCounter++
|
|
1484
|
+
const ei = ctx.varCounter++
|
|
1485
|
+
const evVar = `_eidx${ei}`
|
|
1486
|
+
lines.push(`{let ${evVar}=${baseIdx}`)
|
|
1487
|
+
lines.push(`const _bf${bfi}=[${fns.join(',')}]`)
|
|
1488
|
+
const maxExprs = branchMaxIdx.map((m, i) => `_bi===${i}?${Math.max(m, baseIdx)}`).join(':') + `:${baseIdx}`
|
|
1489
|
+
if (branchKeyword === 'oneOf') {
|
|
1490
|
+
lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){${evVar}=${maxExprs};break}}`)
|
|
1491
|
+
} else {
|
|
1492
|
+
lines.push(`for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){const _m=${maxExprs};if(_m>${evVar})${evVar}=_m}}`)
|
|
1493
|
+
}
|
|
1494
|
+
if (schema.unevaluatedItems === false) {
|
|
1495
|
+
const inner = `if(${v}.length>${evVar})return false`
|
|
1496
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1497
|
+
ctx.deferredChecks.push(isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
|
|
1498
|
+
} else {
|
|
1499
|
+
const ui = ctx.varCounter++
|
|
1500
|
+
const elemVar = `_ue${ui}`
|
|
1501
|
+
const idxVar = `_ui${ui}`
|
|
1502
|
+
const subLines = []
|
|
1503
|
+
genCode(schema.unevaluatedItems, elemVar, subLines, ctx)
|
|
1504
|
+
if (subLines.length > 0) {
|
|
1505
|
+
const check = subLines.join(';')
|
|
1506
|
+
const inner = `for(let ${idxVar}=${evVar};${idxVar}<${v}.length;${idxVar}++){const ${elemVar}=${v}[${idxVar}];${check}}`
|
|
1507
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1508
|
+
ctx.deferredChecks.push(isArr ? inner + '}' : `if(Array.isArray(${v})){${inner}}}`)
|
|
1509
|
+
} else {
|
|
1510
|
+
lines.push('}')
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
} else if (schema.if && (schema.then || schema.else) && (schema.unevaluatedItems === false || typeof schema.unevaluatedItems === 'object')) {
|
|
1514
|
+
// if/then/else: branch-specific max index
|
|
1515
|
+
const ifEval = collectEvaluated(schema.if, ctx.schemaMap, ctx.rootDefs)
|
|
1516
|
+
const thenEval = schema.then ? collectEvaluated(schema.then, ctx.schemaMap, ctx.rootDefs) : { items: null }
|
|
1517
|
+
const elseEval = schema.else ? collectEvaluated(schema.else, ctx.schemaMap, ctx.rootDefs) : { items: null }
|
|
1518
|
+
const ifIdx = ifEval.items || 0
|
|
1519
|
+
const thenIdx = Math.max(baseIdx, ifIdx, thenEval.items || 0)
|
|
1520
|
+
const elseIdx = Math.max(baseIdx, elseEval.items || 0)
|
|
1521
|
+
|
|
1522
|
+
const ifLines2 = []
|
|
1523
|
+
genCode(schema.if, '_iv3', ifLines2, ctx)
|
|
1524
|
+
const ufi = ctx.varCounter++
|
|
1525
|
+
const ifFn3 = ifLines2.length === 0
|
|
1526
|
+
? `function(_iv3){return true}`
|
|
1527
|
+
: `function(_iv3){${ifLines2.join(';')};return true}`
|
|
1528
|
+
|
|
1529
|
+
if (schema.unevaluatedItems === false) {
|
|
1530
|
+
const guard = isArr ? '' : `if(Array.isArray(${v}))`
|
|
1531
|
+
lines.push(`${guard}{const _uif${ufi}=${ifFn3};if(_uif${ufi}(${v})){if(${v}.length>${thenIdx})return false}else{if(${v}.length>${elseIdx})return false}}`)
|
|
1532
|
+
}
|
|
1533
|
+
} else if (schema.unevaluatedItems === false) {
|
|
1534
|
+
// Fallback: use static base index (may not be fully correct for all dynamic cases)
|
|
1535
|
+
const maxIdx = evalResult.items || 0
|
|
1536
|
+
const inner = `if(${v}.length>${maxIdx})return false`
|
|
1537
|
+
if (!ctx.deferredChecks) ctx.deferredChecks = []
|
|
1538
|
+
ctx.deferredChecks.push(isArr ? inner : `if(Array.isArray(${v})){${inner}}`)
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1113
1542
|
}
|
|
1114
1543
|
|
|
1115
1544
|
const FORMAT_CODEGEN = {
|
|
@@ -1172,10 +1601,41 @@ function fastPrefixCheck(pattern, keyVar) {
|
|
|
1172
1601
|
return `${keyVar}.startsWith(${JSON.stringify(prefix)})`
|
|
1173
1602
|
}
|
|
1174
1603
|
|
|
1604
|
+
// Generate a charCodeAt(0)-based switch tree for fast key validation.
|
|
1605
|
+
// V8 compiles switch to jump tables — O(1) dispatch vs O(n) chain.
|
|
1606
|
+
function genCharCodeSwitch(keys, v) {
|
|
1607
|
+
if (keys.length === 0) return `for(var _k in ${v})return false`
|
|
1608
|
+
if (keys.length <= 3) {
|
|
1609
|
+
// Small set: simple chain is faster than switch overhead
|
|
1610
|
+
return `for(var _k in ${v})if(${keys.map(k => `_k!==${JSON.stringify(k)}`).join('&&')})return false`
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// Group keys by first charCode
|
|
1614
|
+
const groups = new Map()
|
|
1615
|
+
for (const k of keys) {
|
|
1616
|
+
const cc = k.charCodeAt(0)
|
|
1617
|
+
if (!groups.has(cc)) groups.set(cc, [])
|
|
1618
|
+
groups.get(cc).push(k)
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
let cases = ''
|
|
1622
|
+
for (const [cc, groupKeys] of groups) {
|
|
1623
|
+
const cond = groupKeys.map(k => `_k===${JSON.stringify(k)}`).join('||')
|
|
1624
|
+
cases += `case ${cc}:if(${cond})continue;break;`
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
return `for(var _k in ${v}){switch(_k.charCodeAt(0)){${cases}default:break}return false}`
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1175
1630
|
// --- Error-collecting codegen: same checks, but pushes errors instead of returning false ---
|
|
1176
1631
|
// Returns a function: (data, allErrors) => { valid, errors }
|
|
1177
1632
|
// Valid path is still fast — only error path does extra work.
|
|
1178
1633
|
function compileToJSCodegenWithErrors(schema, schemaMap) {
|
|
1634
|
+
// Bail on unevaluated keywords — error codegen doesn't support them yet
|
|
1635
|
+
if (typeof schema === 'object' && schema !== null) {
|
|
1636
|
+
const s = JSON.stringify(schema)
|
|
1637
|
+
if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
|
|
1638
|
+
}
|
|
1179
1639
|
if (typeof schema === 'boolean') {
|
|
1180
1640
|
return schema
|
|
1181
1641
|
? () => ({ valid: true, errors: [] })
|
|
@@ -1572,6 +2032,11 @@ function genCodeE(schema, v, pathExpr, lines, ctx) {
|
|
|
1572
2032
|
// Avoids double-pass (jsFn → false → errFn runs same checks again).
|
|
1573
2033
|
// Uses type-aware optimizations: after type check passes, skip guards.
|
|
1574
2034
|
function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
|
|
2035
|
+
// Bail on unevaluated keywords — combined codegen doesn't support them yet
|
|
2036
|
+
if (typeof schema === 'object' && schema !== null) {
|
|
2037
|
+
const s = JSON.stringify(schema)
|
|
2038
|
+
if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
|
|
2039
|
+
}
|
|
1575
2040
|
if (typeof schema === 'boolean') {
|
|
1576
2041
|
return schema
|
|
1577
2042
|
? () => VALID_RESULT
|
|
@@ -2063,4 +2528,124 @@ function genCodeC(schema, v, pathExpr, lines, ctx) {
|
|
|
2063
2528
|
}
|
|
2064
2529
|
}
|
|
2065
2530
|
|
|
2066
|
-
|
|
2531
|
+
// Collect statically-known evaluated properties/items from a schema.
|
|
2532
|
+
// Returns { props: string[], items: number|null, allProps: bool, allItems: bool, dynamic: bool }
|
|
2533
|
+
function collectEvaluated(schema, schemaMap, rootDefs) {
|
|
2534
|
+
if (typeof schema !== 'object' || schema === null) return { props: [], items: null, allProps: false, allItems: false, dynamic: false }
|
|
2535
|
+
const defs = rootDefs || schema.$defs || schema.definitions || null
|
|
2536
|
+
const result = { props: [], items: null, allProps: false, allItems: false, dynamic: false }
|
|
2537
|
+
_collectEval(schema, result, defs, schemaMap, new Set(), true)
|
|
2538
|
+
return result
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
|
|
2542
|
+
if (typeof schema !== 'object' || schema === null) return
|
|
2543
|
+
if (result.allProps && result.allItems) return
|
|
2544
|
+
|
|
2545
|
+
// $ref — inline
|
|
2546
|
+
if (schema.$ref) {
|
|
2547
|
+
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
2548
|
+
if (m && defs && defs[m[1]]) {
|
|
2549
|
+
if (refStack.has(schema.$ref)) { result.dynamic = true; return }
|
|
2550
|
+
refStack.add(schema.$ref)
|
|
2551
|
+
_collectEval(defs[m[1]], result, defs, schemaMap, refStack)
|
|
2552
|
+
refStack.delete(schema.$ref)
|
|
2553
|
+
} else if (schemaMap && typeof schemaMap.get === 'function' && schemaMap.has(schema.$ref)) {
|
|
2554
|
+
if (refStack.has(schema.$ref)) { result.dynamic = true; return }
|
|
2555
|
+
refStack.add(schema.$ref)
|
|
2556
|
+
_collectEval(schemaMap.get(schema.$ref), result, defs, schemaMap, refStack)
|
|
2557
|
+
refStack.delete(schema.$ref)
|
|
2558
|
+
}
|
|
2559
|
+
// In 2020-12, $ref can coexist with siblings — don't return early if there are other keywords
|
|
2560
|
+
const hasOtherKeywords = Object.keys(schema).some(k => k !== '$ref' && k !== '$defs' && k !== 'definitions' && k !== '$schema' && k !== '$id')
|
|
2561
|
+
if (!hasOtherKeywords) return
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
// properties → static keys
|
|
2565
|
+
if (schema.properties) {
|
|
2566
|
+
for (const k of Object.keys(schema.properties)) {
|
|
2567
|
+
if (!result.props.includes(k)) result.props.push(k)
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
// additionalProperties: true/schema → all props evaluated
|
|
2572
|
+
if (schema.additionalProperties !== undefined && schema.additionalProperties !== false) {
|
|
2573
|
+
result.allProps = true
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
// patternProperties → dynamic
|
|
2577
|
+
if (schema.patternProperties) {
|
|
2578
|
+
result.dynamic = true
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
// prefixItems → max index
|
|
2582
|
+
if (schema.prefixItems) {
|
|
2583
|
+
const count = schema.prefixItems.length
|
|
2584
|
+
result.items = result.items === null ? count : Math.max(result.items, count)
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
// items: schema/true → all items evaluated
|
|
2588
|
+
if (schema.items && typeof schema.items === 'object') {
|
|
2589
|
+
result.allItems = true
|
|
2590
|
+
}
|
|
2591
|
+
if (schema.items === true) {
|
|
2592
|
+
result.allItems = true
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
// contains interaction with unevaluatedItems is complex
|
|
2596
|
+
// At root level: contains + unevaluatedItems needs dynamic tracking
|
|
2597
|
+
// In nested schemas: contains marks all items as evaluated
|
|
2598
|
+
if (schema.contains) {
|
|
2599
|
+
if (isRoot && (schema.unevaluatedItems !== undefined)) {
|
|
2600
|
+
result.dynamic = true
|
|
2601
|
+
} else {
|
|
2602
|
+
result.allItems = true
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// unevaluatedProperties: true/schema → all props evaluated (for nested schemas only)
|
|
2607
|
+
// At root level, unevaluatedProperties is what we're computing FOR, not a contributor
|
|
2608
|
+
if (!isRoot && (schema.unevaluatedProperties === true || (typeof schema.unevaluatedProperties === 'object' && schema.unevaluatedProperties !== null))) {
|
|
2609
|
+
result.allProps = true
|
|
2610
|
+
}
|
|
2611
|
+
// unevaluatedItems: true/schema → all items evaluated (for nested schemas only)
|
|
2612
|
+
if (!isRoot && (schema.unevaluatedItems === true || (typeof schema.unevaluatedItems === 'object' && schema.unevaluatedItems !== null))) {
|
|
2613
|
+
result.allItems = true
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
// allOf → merge all (unconditional)
|
|
2617
|
+
if (schema.allOf) {
|
|
2618
|
+
for (const sub of schema.allOf) {
|
|
2619
|
+
_collectEval(sub, result, defs, schemaMap, refStack)
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
// anyOf / oneOf → dynamic (conditional merge)
|
|
2624
|
+
if (schema.anyOf || schema.oneOf) {
|
|
2625
|
+
result.dynamic = true
|
|
2626
|
+
const branches = schema.anyOf || schema.oneOf
|
|
2627
|
+
for (const sub of branches) {
|
|
2628
|
+
_collectEval(sub, result, defs, schemaMap, refStack)
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
// if/then/else → dynamic (branch-dependent)
|
|
2633
|
+
if (schema.if && (schema.then || schema.else)) {
|
|
2634
|
+
result.dynamic = true
|
|
2635
|
+
_collectEval(schema.if, result, defs, schemaMap, refStack)
|
|
2636
|
+
if (schema.then) _collectEval(schema.then, result, defs, schemaMap, refStack)
|
|
2637
|
+
if (schema.else) _collectEval(schema.else, result, defs, schemaMap, refStack)
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
// dependentSchemas → dynamic
|
|
2641
|
+
if (schema.dependentSchemas) {
|
|
2642
|
+
result.dynamic = true
|
|
2643
|
+
for (const sub of Object.values(schema.dependentSchemas)) {
|
|
2644
|
+
_collectEval(sub, result, defs, schemaMap, refStack)
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// not → contributes nothing (spec: annotations from not are discarded)
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated }
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Ultra-fast JSON Schema validator
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "Ultra-fast JSON Schema validator with full ajv feature parity. 5.9x faster validation on complex schemas, 2,184x faster compilation. Cross-schema $ref, Draft 7 support, patternProperties/dependentSchemas/propertyNames codegen. V8-optimized JS codegen, simdjson, RE2, multi-core. Standard Schema V1 compatible.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"scripts": {
|
|
Binary file
|