ata-validator 0.4.14 → 0.4.16
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 +93 -16
- package/index.d.ts +4 -0
- package/index.js +147 -31
- package/lib/draft7.js +82 -0
- package/lib/js-compiler.js +563 -62
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -2,21 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjson/simdjson). Multi-core parallel validation, RE2 regex, codegen bytecode engine. Standard Schema V1 compatible.
|
|
4
4
|
|
|
5
|
-
**[ata-validator.com](https://ata-validator.com)**
|
|
5
|
+
**[ata-validator.com](https://ata-validator.com)** | **[API Docs](docs/API.md)** | **[Contributing](CONTRIBUTING.md)**
|
|
6
6
|
|
|
7
7
|
## Performance
|
|
8
8
|
|
|
9
|
-
###
|
|
9
|
+
### Simple Schema (5 properties, type + format + range checks)
|
|
10
10
|
|
|
11
11
|
| Scenario | ata | ajv | |
|
|
12
12
|
|---|---|---|---|
|
|
13
|
-
| **validate(obj)** valid |
|
|
14
|
-
| **validate(obj)** invalid |
|
|
15
|
-
| **isValidObject(obj)** |
|
|
16
|
-
| **
|
|
17
|
-
| **First validation** |
|
|
13
|
+
| **validate(obj)** valid | 28ns | 104ns | **ata 3.6x faster** |
|
|
14
|
+
| **validate(obj)** invalid | 79ns | 108ns | **ata 2.3x faster** |
|
|
15
|
+
| **isValidObject(obj)** | 28ns | 102ns | **ata 3.7x faster** |
|
|
16
|
+
| **Schema compilation** | 554ns | 1.21ms | **ata 2,184x faster** |
|
|
17
|
+
| **First validation** | 1.70μs | 1.18ms | **ata 719x faster** |
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
### Complex Schema (patternProperties + dependentSchemas + propertyNames + additionalProperties)
|
|
20
|
+
|
|
21
|
+
| Scenario | ata | ajv | |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| **validate(obj)** valid | 20ns | 121ns | **ata 5.9x faster** |
|
|
24
|
+
| **validate(obj)** invalid | 53ns | 196ns | **ata 3.2x faster** |
|
|
25
|
+
| **isValidObject(obj)** | 20ns | 128ns | **ata 5.9x faster** |
|
|
26
|
+
|
|
27
|
+
### Cross-Schema `$ref` (multi-schema with `$id` registry)
|
|
28
|
+
|
|
29
|
+
| Scenario | ata | ajv | |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| **validate(obj)** valid | 17ns | 25ns | **ata 1.5x faster** |
|
|
32
|
+
| **validate(obj)** invalid | 34ns | 54ns | **ata 1.6x faster** |
|
|
33
|
+
|
|
34
|
+
> Measured with [mitata](https://github.com/evanwashere/mitata) on Apple M4 Pro (process-isolated). [Benchmark code](benchmark/bench_complex_mitata.mjs)
|
|
35
|
+
|
|
36
|
+
### vs Ecosystem (Zod, Valibot, TypeBox)
|
|
37
|
+
|
|
38
|
+
| Scenario | ata | ajv | typebox | zod | valibot |
|
|
39
|
+
|---|---|---|---|---|---|
|
|
40
|
+
| **validate (valid)** | **13ns** | 37ns | 48ns | 328ns | 316ns |
|
|
41
|
+
| **validate (invalid)** | **35ns** | 104ns | 4ns | 11.7μs | 838ns |
|
|
42
|
+
| **compilation** | **533ns** | 1.14ms | 52μs | — | — |
|
|
43
|
+
| **first validation** | **1.3μs** | 1.07ms | 53μs | — | — |
|
|
44
|
+
|
|
45
|
+
> Different categories: ata/ajv/typebox are JSON Schema validators, zod/valibot are schema-builder DSLs. [Benchmark code](benchmark/bench_all_mitata.mjs)
|
|
20
46
|
|
|
21
47
|
### Large Data - JS Object Validation
|
|
22
48
|
|
|
@@ -39,11 +65,11 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
39
65
|
|
|
40
66
|
### How it works
|
|
41
67
|
|
|
42
|
-
**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 - no double validation, no try/catch (3.3x V8 deopt). Lazy compilation defers all work to first usage - constructor is near-zero cost.
|
|
68
|
+
**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.
|
|
43
69
|
|
|
44
|
-
**JS codegen**: Schemas are compiled to monolithic JS functions (like ajv).
|
|
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`).
|
|
45
71
|
|
|
46
|
-
**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).
|
|
72
|
+
**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).
|
|
47
73
|
|
|
48
74
|
**Adaptive simdjson**: For large documents (>8KB) with selective schemas, simdjson On Demand seeks only the needed fields - skipping irrelevant data at GB/s speeds.
|
|
49
75
|
|
|
@@ -53,8 +79,11 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
53
79
|
|
|
54
80
|
## When to use ata
|
|
55
81
|
|
|
56
|
-
- **High-throughput `validate(obj)`** -
|
|
57
|
-
- **
|
|
82
|
+
- **High-throughput `validate(obj)`** - 5.9x faster than ajv on complex schemas, 27x faster than zod
|
|
83
|
+
- **Complex schemas** - `patternProperties`, `dependentSchemas`, `propertyNames` all inline JS codegen (5.9x faster than ajv)
|
|
84
|
+
- **Multi-schema projects** - cross-schema `$ref` with `$id` registry, `addSchema()` API
|
|
85
|
+
- **Draft 7 migration** - auto-detects `$schema`, normalizes Draft 7 keywords transparently
|
|
86
|
+
- **Serverless / cold starts** - 2,184x faster compilation, 719x faster first validation
|
|
58
87
|
- **Security-sensitive apps** - RE2 regex, immune to ReDoS attacks
|
|
59
88
|
- **Batch/streaming validation** - NDJSON log processing, data pipelines (2.6x faster)
|
|
60
89
|
- **Standard Schema V1** - native support for Fastify v5, tRPC, TanStack
|
|
@@ -62,12 +91,14 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
62
91
|
|
|
63
92
|
## When to use ajv
|
|
64
93
|
|
|
65
|
-
- **Schemas with `patternProperties`, `dependentSchemas`** - these bypass JS codegen and hit the slower NAPI path
|
|
66
94
|
- **100% spec compliance needed** - ajv covers more edge cases (ata: 98.4%)
|
|
95
|
+
- **`$dynamicRef` / `unevaluatedProperties`** - not yet supported in ata
|
|
67
96
|
|
|
68
97
|
## Features
|
|
69
98
|
|
|
70
|
-
- **Hybrid validator**:
|
|
99
|
+
- **Hybrid validator**: 5.9x faster than ajv valid, 3.2x 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
|
+
- **Cross-schema `$ref`**: `schemas` option and `addSchema()` API. Compile-time resolution with `$id` registry, zero runtime overhead
|
|
101
|
+
- **Draft 7 support**: Auto-detects `$schema` field, normalizes `dependencies`/`additionalItems`/`definitions` transparently
|
|
71
102
|
- **Multi-core**: Parallel validation across all CPU cores - 13.4M validations/sec
|
|
72
103
|
- **simdjson**: SIMD-accelerated JSON parsing at GB/s speeds, adaptive On Demand for large docs
|
|
73
104
|
- **RE2 regex**: Linear-time guarantees, immune to ReDoS attacks (2391x faster on pathological input)
|
|
@@ -122,12 +153,36 @@ v.isValidParallel(ndjson); // bool[]
|
|
|
122
153
|
v.countValid(ndjson); // number
|
|
123
154
|
```
|
|
124
155
|
|
|
156
|
+
### Cross-Schema `$ref`
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
const addressSchema = {
|
|
160
|
+
$id: 'https://example.com/address',
|
|
161
|
+
type: 'object',
|
|
162
|
+
properties: { street: { type: 'string' }, city: { type: 'string' } },
|
|
163
|
+
required: ['street', 'city']
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const v = new Validator({
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
name: { type: 'string' },
|
|
170
|
+
address: { $ref: 'https://example.com/address' }
|
|
171
|
+
}
|
|
172
|
+
}, { schemas: [addressSchema] });
|
|
173
|
+
|
|
174
|
+
// Or use addSchema()
|
|
175
|
+
const v2 = new Validator(mainSchema);
|
|
176
|
+
v2.addSchema(addressSchema);
|
|
177
|
+
```
|
|
178
|
+
|
|
125
179
|
### Options
|
|
126
180
|
|
|
127
181
|
```javascript
|
|
128
182
|
const v = new Validator(schema, {
|
|
129
183
|
coerceTypes: true, // "42" → 42 for integer fields
|
|
130
184
|
removeAdditional: true, // strip properties not in schema
|
|
185
|
+
schemas: [otherSchema], // cross-schema $ref registry
|
|
131
186
|
});
|
|
132
187
|
```
|
|
133
188
|
|
|
@@ -215,6 +270,27 @@ auto result = ata::validate(schema, R"({"name": "Mert"})");
|
|
|
215
270
|
|
|
216
271
|
## Building from Source
|
|
217
272
|
|
|
273
|
+
### Development prerequisites
|
|
274
|
+
|
|
275
|
+
Native builds require C/C++ toolchain support and the following libraries:
|
|
276
|
+
|
|
277
|
+
- `re2`
|
|
278
|
+
- `abseil`
|
|
279
|
+
- `mimalloc`
|
|
280
|
+
|
|
281
|
+
Install them before running `npm install` / `npm run build`:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
# macOS (Homebrew)
|
|
285
|
+
brew install re2 abseil mimalloc
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
# Ubuntu/Debian (apt)
|
|
290
|
+
sudo apt-get update
|
|
291
|
+
sudo apt-get install -y libre2-dev libabsl-dev libmimalloc-dev
|
|
292
|
+
```
|
|
293
|
+
|
|
218
294
|
```bash
|
|
219
295
|
# C++ library + tests
|
|
220
296
|
cmake -B build
|
|
@@ -234,6 +310,7 @@ npm run test:suite
|
|
|
234
310
|
|
|
235
311
|
MIT
|
|
236
312
|
|
|
237
|
-
##
|
|
313
|
+
## Authors
|
|
238
314
|
|
|
239
315
|
[Mert Can Altin](https://github.com/mertcanaltin)
|
|
316
|
+
[Daniel Lemire](https://github.com/lemire)
|
package/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface ValidationResult {
|
|
|
12
12
|
export interface ValidatorOptions {
|
|
13
13
|
coerceTypes?: boolean;
|
|
14
14
|
removeAdditional?: boolean;
|
|
15
|
+
schemas?: Record<string, object> | object[];
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export interface StandardSchemaV1Props {
|
|
@@ -27,6 +28,9 @@ export interface StandardSchemaV1Props {
|
|
|
27
28
|
export class Validator {
|
|
28
29
|
constructor(schema: object | string, options?: ValidatorOptions);
|
|
29
30
|
|
|
31
|
+
/** Add a schema to the validator */
|
|
32
|
+
addSchema(schema: object): void;
|
|
33
|
+
|
|
30
34
|
/** Validate data — returns result with errors. Applies defaults, coerceTypes, removeAdditional. */
|
|
31
35
|
validate(data: unknown): ValidationResult;
|
|
32
36
|
|
package/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
compileToJSCodegenWithErrors,
|
|
6
6
|
compileToJSCombined,
|
|
7
7
|
} = require("./lib/js-compiler");
|
|
8
|
+
const { normalizeDraft7 } = require("./lib/draft7");
|
|
8
9
|
|
|
9
10
|
// Extract default values from a schema tree. Returns a function that applies
|
|
10
11
|
// defaults to an object in-place (mutates), or null if no defaults exist.
|
|
@@ -205,6 +206,58 @@ function collectRemovals(schema, actions, path) {
|
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
// Generate a fast preprocess function via codegen instead of closure arrays
|
|
210
|
+
function buildPreprocessCodegen(schema, options) {
|
|
211
|
+
if (typeof schema !== 'object' || schema === null || !schema.properties) return null;
|
|
212
|
+
const lines = [];
|
|
213
|
+
const props = schema.properties;
|
|
214
|
+
const keys = Object.keys(props);
|
|
215
|
+
|
|
216
|
+
// removeAdditional: inline key check
|
|
217
|
+
if (options.removeAdditional && schema.additionalProperties === false) {
|
|
218
|
+
const checks = keys.map(k => `_k!==${JSON.stringify(k)}`).join('&&');
|
|
219
|
+
lines.push(`for(var _k in d)if(${checks})delete d[_k]`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// coerceTypes: inline per property
|
|
223
|
+
if (options.coerceTypes) {
|
|
224
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
225
|
+
if (!prop || typeof prop !== 'object' || !prop.type) continue;
|
|
226
|
+
const t = Array.isArray(prop.type) ? null : prop.type;
|
|
227
|
+
if (!t) continue;
|
|
228
|
+
const k = JSON.stringify(key);
|
|
229
|
+
if (t === 'integer') {
|
|
230
|
+
lines.push(`if(typeof d[${k}]==='string'){var _n=Number(d[${k}]);if(d[${k}]!==''&&Number.isInteger(_n))d[${k}]=_n}`);
|
|
231
|
+
lines.push(`if(typeof d[${k}]==='boolean')d[${k}]=d[${k}]?1:0`);
|
|
232
|
+
} else if (t === 'number') {
|
|
233
|
+
lines.push(`if(typeof d[${k}]==='string'){var _n=Number(d[${k}]);if(d[${k}]!==''&&!isNaN(_n))d[${k}]=_n}`);
|
|
234
|
+
lines.push(`if(typeof d[${k}]==='boolean')d[${k}]=d[${k}]?1:0`);
|
|
235
|
+
} else if (t === 'string') {
|
|
236
|
+
lines.push(`if(typeof d[${k}]==='number'||typeof d[${k}]==='boolean')d[${k}]=String(d[${k}])`);
|
|
237
|
+
} else if (t === 'boolean') {
|
|
238
|
+
lines.push(`if(d[${k}]==='true'||d[${k}]==='1')d[${k}]=true`);
|
|
239
|
+
lines.push(`if(d[${k}]==='false'||d[${k}]==='0')d[${k}]=false`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// defaults: inline per property
|
|
245
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
246
|
+
if (prop && typeof prop === 'object' && prop.default !== undefined) {
|
|
247
|
+
const k = JSON.stringify(key);
|
|
248
|
+
const def = JSON.stringify(prop.default);
|
|
249
|
+
lines.push(`if(!(${k} in d))d[${k}]=${def}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (lines.length === 0) return null;
|
|
254
|
+
try {
|
|
255
|
+
return new Function('d', lines.join('\n'));
|
|
256
|
+
} catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
208
261
|
// Schema compilation cache: same schema string -> reuse compiled functions
|
|
209
262
|
const _compileCache = new Map();
|
|
210
263
|
|
|
@@ -233,13 +286,35 @@ function createPaddedBuffer(jsonStr) {
|
|
|
233
286
|
return { buffer: padded, length: jsonBuf.length };
|
|
234
287
|
}
|
|
235
288
|
|
|
289
|
+
function buildSchemaMap(schemas) {
|
|
290
|
+
if (!schemas) return null
|
|
291
|
+
const map = new Map()
|
|
292
|
+
if (Array.isArray(schemas)) {
|
|
293
|
+
for (const s of schemas) {
|
|
294
|
+
normalizeDraft7(s)
|
|
295
|
+
const id = s.$id
|
|
296
|
+
if (!id) throw new Error('Schema in schemas option must have $id')
|
|
297
|
+
map.set(id, s)
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
for (const [key, s] of Object.entries(schemas)) {
|
|
301
|
+
normalizeDraft7(s)
|
|
302
|
+
map.set(s.$id || key, s)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return map
|
|
306
|
+
}
|
|
307
|
+
|
|
236
308
|
class Validator {
|
|
237
309
|
constructor(schema, opts) {
|
|
238
310
|
const options = opts || {};
|
|
239
|
-
const schemaStr =
|
|
240
|
-
typeof schema === "string" ? schema : JSON.stringify(schema);
|
|
241
311
|
const schemaObj = typeof schema === "string" ? JSON.parse(schema) : schema;
|
|
242
312
|
|
|
313
|
+
// Draft 7 normalization — convert keywords to 2020-12 equivalents in-place
|
|
314
|
+
normalizeDraft7(schemaObj);
|
|
315
|
+
|
|
316
|
+
const schemaStr = JSON.stringify(schemaObj);
|
|
317
|
+
|
|
243
318
|
this._schemaStr = schemaStr;
|
|
244
319
|
this._schemaObj = schemaObj;
|
|
245
320
|
this._options = options;
|
|
@@ -251,6 +326,9 @@ class Validator {
|
|
|
251
326
|
this._preprocess = null;
|
|
252
327
|
this._applyDefaults = null;
|
|
253
328
|
|
|
329
|
+
// Schema map for cross-schema $ref resolution
|
|
330
|
+
this._schemaMap = buildSchemaMap(options.schemas) || new Map();
|
|
331
|
+
|
|
254
332
|
// Lazy stubs: trigger compilation on first call, then re-dispatch
|
|
255
333
|
this.validate = (data) => {
|
|
256
334
|
this._ensureCompiled();
|
|
@@ -303,40 +381,45 @@ class Validator {
|
|
|
303
381
|
const options = this._options;
|
|
304
382
|
|
|
305
383
|
// Check cache first -- reuse compiled functions for same schema
|
|
306
|
-
const
|
|
384
|
+
const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
|
|
385
|
+
const mapKey = this._schemaMap.size > 0
|
|
386
|
+
? this._schemaStr + '\0' + [...this._schemaMap.keys()].sort().join('\0')
|
|
387
|
+
: this._schemaStr;
|
|
388
|
+
const cached = _compileCache.get(mapKey);
|
|
307
389
|
let jsFn, jsCombinedFn, jsErrFn;
|
|
308
390
|
if (cached && !process.env.ATA_FORCE_NAPI) {
|
|
309
391
|
jsFn = cached.jsFn;
|
|
310
392
|
jsCombinedFn = cached.combined;
|
|
311
393
|
jsErrFn = cached.errFn;
|
|
312
394
|
} else if (!process.env.ATA_FORCE_NAPI) {
|
|
313
|
-
jsFn = compileToJSCodegen(schemaObj) || compileToJS(schemaObj);
|
|
314
|
-
jsCombinedFn = compileToJSCombined(schemaObj, VALID_RESULT);
|
|
315
|
-
jsErrFn = compileToJSCodegenWithErrors(schemaObj);
|
|
316
|
-
_compileCache.set(
|
|
395
|
+
jsFn = compileToJSCodegen(schemaObj, sm) || compileToJS(schemaObj, null, sm);
|
|
396
|
+
jsCombinedFn = compileToJSCombined(schemaObj, VALID_RESULT, sm);
|
|
397
|
+
jsErrFn = compileToJSCodegenWithErrors(schemaObj, sm);
|
|
398
|
+
_compileCache.set(mapKey, { jsFn, combined: jsCombinedFn, errFn: jsErrFn });
|
|
317
399
|
} else {
|
|
318
400
|
jsFn = null; jsCombinedFn = null; jsErrFn = null;
|
|
319
401
|
}
|
|
320
402
|
this._jsFn = jsFn;
|
|
321
403
|
|
|
322
|
-
// Data mutators --
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
?
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
404
|
+
// Data mutators -- try codegen first (12x faster), fallback to closure arrays
|
|
405
|
+
let preprocess = buildPreprocessCodegen(schemaObj, options);
|
|
406
|
+
if (!preprocess) {
|
|
407
|
+
const applyDefaults = buildDefaultsApplier(schemaObj);
|
|
408
|
+
const applyCoerce = options.coerceTypes ? buildCoercer(schemaObj) : null;
|
|
409
|
+
const applyRemove = options.removeAdditional
|
|
410
|
+
? buildRemover(schemaObj)
|
|
411
|
+
: null;
|
|
412
|
+
const mutators = [applyRemove, applyCoerce, applyDefaults].filter(Boolean);
|
|
413
|
+
preprocess =
|
|
414
|
+
mutators.length === 0
|
|
415
|
+
? null
|
|
416
|
+
: mutators.length === 1
|
|
417
|
+
? mutators[0]
|
|
418
|
+
: (data) => {
|
|
419
|
+
for (let i = 0; i < mutators.length; i++) mutators[i](data);
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
this._applyDefaults = preprocess;
|
|
340
423
|
this._preprocess = preprocess;
|
|
341
424
|
|
|
342
425
|
// Detect if schema is "selective" -- doesn't recurse into arrays/deep objects.
|
|
@@ -394,7 +477,15 @@ class Validator {
|
|
|
394
477
|
} catch {}
|
|
395
478
|
}
|
|
396
479
|
|
|
397
|
-
if (safeCombinedFn) {
|
|
480
|
+
if (safeCombinedFn && jsFn) {
|
|
481
|
+
// Hybrid: jsFn boolean guard for valid (fast, no allocation), combined for invalid
|
|
482
|
+
this.validate = preprocess
|
|
483
|
+
? (data) => {
|
|
484
|
+
preprocess(data);
|
|
485
|
+
return jsFn(data) ? VALID_RESULT : safeCombinedFn(data);
|
|
486
|
+
}
|
|
487
|
+
: (data) => jsFn(data) ? VALID_RESULT : safeCombinedFn(data);
|
|
488
|
+
} else if (safeCombinedFn) {
|
|
398
489
|
this.validate = preprocess
|
|
399
490
|
? (data) => {
|
|
400
491
|
preprocess(data);
|
|
@@ -494,25 +585,50 @@ class Validator {
|
|
|
494
585
|
_ensureNative() {
|
|
495
586
|
if (this._nativeReady) return;
|
|
496
587
|
this._nativeReady = true;
|
|
497
|
-
|
|
498
|
-
this.
|
|
588
|
+
let nativeSchemaStr = this._schemaStr;
|
|
589
|
+
if (this._schemaMap.size > 0) {
|
|
590
|
+
const merged = JSON.parse(this._schemaStr);
|
|
591
|
+
if (!merged.$defs) merged.$defs = {};
|
|
592
|
+
for (const [id, s] of this._schemaMap) {
|
|
593
|
+
merged.$defs['__ext_' + id.replace(/[^a-zA-Z0-9]/g, '_')] = s;
|
|
594
|
+
}
|
|
595
|
+
nativeSchemaStr = JSON.stringify(merged);
|
|
596
|
+
}
|
|
597
|
+
this._compiled = new native.CompiledSchema(nativeSchemaStr);
|
|
598
|
+
this._fastSlot = native.fastRegister(nativeSchemaStr);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
addSchema(schema) {
|
|
602
|
+
if (this._initialized) {
|
|
603
|
+
throw new Error('Cannot add schema after compilation — call addSchema() before validate()')
|
|
604
|
+
}
|
|
605
|
+
if (!schema || !schema.$id) {
|
|
606
|
+
throw new Error('Schema must have $id')
|
|
607
|
+
}
|
|
608
|
+
// Apply Draft 7 normalization if needed
|
|
609
|
+
normalizeDraft7(schema)
|
|
610
|
+
this._schemaMap.set(schema.$id, schema)
|
|
499
611
|
}
|
|
500
612
|
|
|
501
613
|
_ensureCodegen() {
|
|
502
614
|
if (this._jsFn) return;
|
|
503
615
|
if (process.env.ATA_FORCE_NAPI) return;
|
|
504
|
-
const
|
|
616
|
+
const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
|
|
617
|
+
const mapKey = this._schemaMap.size > 0
|
|
618
|
+
? this._schemaStr + '\0' + [...this._schemaMap.keys()].sort().join('\0')
|
|
619
|
+
: this._schemaStr;
|
|
620
|
+
const cached = _compileCache.get(mapKey);
|
|
505
621
|
if (cached && cached.jsFn) {
|
|
506
622
|
this._jsFn = cached.jsFn;
|
|
507
623
|
this.isValidObject = cached.jsFn;
|
|
508
624
|
return;
|
|
509
625
|
}
|
|
510
|
-
const jsFn = compileToJSCodegen(this._schemaObj) || compileToJS(this._schemaObj);
|
|
626
|
+
const jsFn = compileToJSCodegen(this._schemaObj, sm) || compileToJS(this._schemaObj, null, sm);
|
|
511
627
|
this._jsFn = jsFn;
|
|
512
628
|
if (jsFn) {
|
|
513
629
|
this.isValidObject = jsFn;
|
|
514
630
|
// seed cache with codegen, combined/errFn filled later by _ensureCompiled
|
|
515
|
-
if (!cached) _compileCache.set(
|
|
631
|
+
if (!cached) _compileCache.set(mapKey, { jsFn, combined: null, errFn: null });
|
|
516
632
|
else cached.jsFn = jsFn;
|
|
517
633
|
}
|
|
518
634
|
}
|
package/lib/draft7.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const DRAFT7_SCHEMAS = new Set([
|
|
4
|
+
'http://json-schema.org/draft-07/schema#',
|
|
5
|
+
'http://json-schema.org/draft-07/schema',
|
|
6
|
+
])
|
|
7
|
+
|
|
8
|
+
function isDraft7(schema) {
|
|
9
|
+
return !!(schema && schema.$schema && DRAFT7_SCHEMAS.has(schema.$schema))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeDraft7(schema) {
|
|
13
|
+
if (!isDraft7(schema)) return schema
|
|
14
|
+
_normalize(schema)
|
|
15
|
+
return schema
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function _normalize(schema) {
|
|
19
|
+
if (typeof schema !== 'object' || schema === null) return
|
|
20
|
+
|
|
21
|
+
// definitions → $defs
|
|
22
|
+
if (schema.definitions && !schema.$defs) {
|
|
23
|
+
schema.$defs = schema.definitions
|
|
24
|
+
delete schema.definitions
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// dependencies → dependentSchemas + dependentRequired
|
|
28
|
+
if (schema.dependencies) {
|
|
29
|
+
for (const [key, value] of Object.entries(schema.dependencies)) {
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
if (!schema.dependentRequired) schema.dependentRequired = {}
|
|
32
|
+
schema.dependentRequired[key] = value
|
|
33
|
+
} else {
|
|
34
|
+
if (!schema.dependentSchemas) schema.dependentSchemas = {}
|
|
35
|
+
schema.dependentSchemas[key] = value
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
delete schema.dependencies
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// items (array form) → prefixItems + items/additionalItems swap
|
|
42
|
+
if (Array.isArray(schema.items)) {
|
|
43
|
+
schema.prefixItems = schema.items
|
|
44
|
+
if (schema.additionalItems !== undefined) {
|
|
45
|
+
schema.items = schema.additionalItems
|
|
46
|
+
delete schema.additionalItems
|
|
47
|
+
} else {
|
|
48
|
+
delete schema.items
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Recurse into object-valued sub-schemas
|
|
53
|
+
const objSubs = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
|
|
54
|
+
for (const key of objSubs) {
|
|
55
|
+
if (schema[key] && typeof schema[key] === 'object') {
|
|
56
|
+
for (const v of Object.values(schema[key])) {
|
|
57
|
+
if (typeof v === 'object' && v !== null) _normalize(v)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Recurse into array-valued sub-schemas
|
|
63
|
+
const arrSubs = ['allOf', 'anyOf', 'oneOf', 'prefixItems']
|
|
64
|
+
for (const key of arrSubs) {
|
|
65
|
+
if (Array.isArray(schema[key])) {
|
|
66
|
+
for (const s of schema[key]) {
|
|
67
|
+
if (typeof s === 'object' && s !== null) _normalize(s)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Recurse into single sub-schemas
|
|
73
|
+
const singleSubs = ['items', 'contains', 'not', 'if', 'then', 'else',
|
|
74
|
+
'additionalProperties', 'propertyNames']
|
|
75
|
+
for (const key of singleSubs) {
|
|
76
|
+
if (typeof schema[key] === 'object' && schema[key] !== null) {
|
|
77
|
+
_normalize(schema[key])
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { isDraft7, normalizeDraft7 }
|