ata-validator 0.4.15 → 0.5.0

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 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
- ### Single-Document Validation
9
+ ### Simple Schema (5 properties, type + format + range checks)
10
10
 
11
11
  | Scenario | ata | ajv | |
12
12
  |---|---|---|---|
13
- | **validate(obj)** valid | 16.6M ops/sec | 9.8M ops/sec | **ata 1.7x faster** |
14
- | **validate(obj)** invalid | 10.0M ops/sec | 5.6M ops/sec | **ata 1.8x faster** |
15
- | **isValidObject(obj)** | 36.0M ops/sec | 9.4M ops/sec | **ata 3.8x faster** |
16
- | **Schema compilation** | 1.6M ops/sec | 781 ops/sec | **ata 2,067x faster** |
17
- | **First validation** | 614K ops/sec | 775 ops/sec | **ata 793x faster** |
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
- > Measured with [mitata](https://github.com/evanwashere/mitata) (process-isolated). Results vary by workload and hardware.
19
+ ### Complex Schema (patternProperties + dependentSchemas + propertyNames + additionalProperties)
20
20
 
21
- ### vs typebox 1.x (with format: 'email')
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
- | Scenario | ata | typebox | |
27
+ ### Cross-Schema `$ref` (multi-schema with `$id` registry)
28
+
29
+ | Scenario | ata | ajv | |
24
30
  |---|---|---|---|
25
- | **isValidObject(obj)** valid | 90.9M ops/sec | 18.2M ops/sec | **ata 5.0x faster** |
26
- | **isValidObject(obj)** invalid | 435M ops/sec | 169M ops/sec | **ata 2.6x faster** |
27
- | **Schema compilation** | 2.0M ops/sec | 18.5K ops/sec | **ata 110x faster** |
28
- | **First validation** | 1.55M ops/sec | 18.2K ops/sec | **ata 85x faster** |
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 1.x is JSON Schema compliant with RFC format support. [Benchmark code](benchmark/bench_vs_typebox_esm.mjs)
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). Supported keywords: `type`, `required`, `properties`, `items`, `enum`, `const`, `allOf`, `anyOf`, `oneOf`, `not`, `if/then/else`, `uniqueItems`, `contains`, `prefixItems`, `additionalProperties`, `dependentRequired`, `$ref` (local), `minimum/maximum`, `minLength/maxLength`, `pattern`, `format`.
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)`** - 16.6M ops/sec valid, 10.0M ops/sec invalid
68
- - **Serverless / cold starts** - 2,067x faster compilation, 793x faster first validation
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**: 16.6M ops/sec valid, 10.0M ops/sec invalid - codegen + single-pass error collection. No try/catch, no double pass. Schema compilation cache for repeated schemas
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 cached = _compileCache.get(this._schemaStr);
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(this._schemaStr, { jsFn, combined: jsCombinedFn, errFn: jsErrFn });
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 -- applied in-place before validation
323
- const applyDefaults = buildDefaultsApplier(schemaObj);
324
- const applyCoerce = options.coerceTypes ? buildCoercer(schemaObj) : null;
325
- const applyRemove = options.removeAdditional
326
- ? buildRemover(schemaObj)
327
- : null;
328
- this._applyDefaults = applyDefaults;
329
-
330
- // Combine all mutators into a single pre-validation step
331
- const mutators = [applyRemove, applyCoerce, applyDefaults].filter(Boolean);
332
- const preprocess =
333
- mutators.length === 0
334
- ? null
335
- : mutators.length === 1
336
- ? mutators[0]
337
- : (data) => {
338
- for (let i = 0; i < mutators.length; i++) mutators[i](data);
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
- this._compiled = new native.CompiledSchema(this._schemaStr);
498
- this._fastSlot = native.fastRegister(this._schemaStr);
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 cached = _compileCache.get(this._schemaStr);
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(this._schemaStr, { jsFn, combined: null, errFn: null });
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 }