ata-validator 0.4.15 → 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 +86 -21
- 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 +7 -3
- package/prebuilds/darwin-arm64/ata-validator.node +0 -0
- package/prebuilds/linux-arm64/ata-validator.node +0 -0
- package/prebuilds/linux-x64/ata-validator.node +0 -0
package/README.md
CHANGED
|
@@ -6,28 +6,43 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
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
|
-
| **Schema compilation** |
|
|
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
20
|
|
|
21
|
-
|
|
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** |
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
### Cross-Schema `$ref` (multi-schema with `$id` registry)
|
|
28
|
+
|
|
29
|
+
| Scenario | ata | ajv | |
|
|
24
30
|
|---|---|---|---|
|
|
25
|
-
| **
|
|
26
|
-
| **
|
|
27
|
-
|
|
28
|
-
|
|
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 | — | — |
|
|
29
44
|
|
|
30
|
-
> typebox
|
|
45
|
+
> Different categories: ata/ajv/typebox are JSON Schema validators, zod/valibot are schema-builder DSLs. [Benchmark code](benchmark/bench_all_mitata.mjs)
|
|
31
46
|
|
|
32
47
|
### Large Data - JS Object Validation
|
|
33
48
|
|
|
@@ -50,11 +65,11 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
50
65
|
|
|
51
66
|
### How it works
|
|
52
67
|
|
|
53
|
-
**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.
|
|
54
69
|
|
|
55
|
-
**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`).
|
|
56
71
|
|
|
57
|
-
**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).
|
|
58
73
|
|
|
59
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.
|
|
60
75
|
|
|
@@ -64,8 +79,11 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
64
79
|
|
|
65
80
|
## When to use ata
|
|
66
81
|
|
|
67
|
-
- **High-throughput `validate(obj)`** -
|
|
68
|
-
- **
|
|
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
|
|
69
87
|
- **Security-sensitive apps** - RE2 regex, immune to ReDoS attacks
|
|
70
88
|
- **Batch/streaming validation** - NDJSON log processing, data pipelines (2.6x faster)
|
|
71
89
|
- **Standard Schema V1** - native support for Fastify v5, tRPC, TanStack
|
|
@@ -73,12 +91,14 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
73
91
|
|
|
74
92
|
## When to use ajv
|
|
75
93
|
|
|
76
|
-
- **Schemas with `patternProperties`, `dependentSchemas`** - these bypass JS codegen and hit the slower NAPI path
|
|
77
94
|
- **100% spec compliance needed** - ajv covers more edge cases (ata: 98.4%)
|
|
95
|
+
- **`$dynamicRef` / `unevaluatedProperties`** - not yet supported in ata
|
|
78
96
|
|
|
79
97
|
## Features
|
|
80
98
|
|
|
81
|
-
- **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
|
|
82
102
|
- **Multi-core**: Parallel validation across all CPU cores - 13.4M validations/sec
|
|
83
103
|
- **simdjson**: SIMD-accelerated JSON parsing at GB/s speeds, adaptive On Demand for large docs
|
|
84
104
|
- **RE2 regex**: Linear-time guarantees, immune to ReDoS attacks (2391x faster on pathological input)
|
|
@@ -133,12 +153,36 @@ v.isValidParallel(ndjson); // bool[]
|
|
|
133
153
|
v.countValid(ndjson); // number
|
|
134
154
|
```
|
|
135
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
|
+
|
|
136
179
|
### Options
|
|
137
180
|
|
|
138
181
|
```javascript
|
|
139
182
|
const v = new Validator(schema, {
|
|
140
183
|
coerceTypes: true, // "42" → 42 for integer fields
|
|
141
184
|
removeAdditional: true, // strip properties not in schema
|
|
185
|
+
schemas: [otherSchema], // cross-schema $ref registry
|
|
142
186
|
});
|
|
143
187
|
```
|
|
144
188
|
|
|
@@ -226,6 +270,27 @@ auto result = ata::validate(schema, R"({"name": "Mert"})");
|
|
|
226
270
|
|
|
227
271
|
## Building from Source
|
|
228
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
|
+
|
|
229
294
|
```bash
|
|
230
295
|
# C++ library + tests
|
|
231
296
|
cmake -B build
|
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 }
|