ata-validator 1.6.2 → 1.7.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/CHANGELOG.md +26 -0
- package/README.md +6 -5
- package/build.d.ts +12 -0
- package/index.d.ts +7 -0
- package/index.js +58 -13
- package/lib/aot-build.js +1 -0
- package/lib/aot.js +69 -19
- package/lib/buffer-gate.js +132 -0
- package/lib/draft7.js +24 -2
- package/lib/interpreter.js +432 -200
- package/lib/js-compiler.js +184 -53
- package/lib/metaschemas.js +21 -0
- package/lib/safe-regex-source.js +1 -1
- package/lib/safe-regex.js +209 -29
- package/lib/version.js +1 -1
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
|
|
4
4
|
|
|
5
|
+
## 1.7.0 - 2026-08-23
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- The four code generation entry points disagreed with each other on 157 instances of the official suite. Most of it was the closure compiler, the boolean fallback behind `isValidObject()` when the code generator declines a schema: it ignored `unevaluatedProperties` and `unevaluatedItems` outright, treated a self-referencing `$ref: "#"` as always true, skipped `additionalProperties` whenever no `properties` map sat next to it, and passed strings in `date-time`, `time`, `uri` and `duration` format without checking them. Each of those accepted input it should have rejected. It now declines those schemas, so they reach an engine that handles them. The remaining disagreements were wrong rejections shared by both boolean paths: `required`, `minProperties` and `maxProperties` applied to non-objects, `multipleOf` had no tolerance for fractional divisors, `const` and `enum` compared objects by key order, and `items` started at index 0 when `prefixItems` was present. `isValidObject()` and `validate()` now agree on every suite instance.
|
|
10
|
+
- A `dependentSchemas` branch carrying `additionalProperties: false` had that check hoisted out of its condition to the top level of the compiled function, so the restriction applied whether or not the triggering property was present.
|
|
11
|
+
- The error path counted a property matched by `patternProperties` as additional under `additionalProperties: false`. The same path now reports the real pointer (`#/patternProperties/<pattern>/...`) for a failing pattern subschema instead of the synthetic `#/patternProperties`, so source frames resolve for those errors.
|
|
12
|
+
- The buffer APIs (`isValid`, `isValidJSON` above the simdjson threshold, `countValid`, `batchIsValid`, `isValidNDJSON`, `isValidParallel`, `isValidPrepadded`) disagreed with `validate()` on 245 of 2222 suite cases, and in 195 of those they accepted a document `validate()` rejects. The native walker behind them does not handle `contains`, `unevaluated*`, `dependencies`, `dependentSchemas`, `dependentRequired`, `propertyNames`, `patternProperties`, tuple-form `items` and `prefixItems`, cross-document `$ref`, embedded `$id`, an empty `enum`, a boolean root schema, Unicode property escapes in `pattern`, or the `hostname`, `date-time`, `time`, `uri-reference` and `duration` formats. For a schema using any of those, every buffer API now parses the bytes and answers through `validate()`; `lib/buffer-gate.js` holds the list. Schemas without them keep the zero-copy path. `tests/test_buffer_path_parity.js` now compares all three dialects, 3359 cases, and holds the disagreement count at zero.
|
|
13
|
+
- A value failing a `patternProperties` subschema was reported at runtime as a generic "value invalid for key" error on the parent object with the synthetic pointer `#/patternProperties`. The subschema is now generated in place, so the error comes from its own keyword, at the key's path (`/x-flag`), with the real pointer (`#/patternProperties/^x-/type`), and source frames resolve for it. Patterns containing `/` or `'` produce a correctly escaped pointer.
|
|
14
|
+
- `tests/test_codegen_entrypoint_agreement.js` drives the whole suite through each entry point directly and fails on any split verdict. It runs as part of `npm test`.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- A `$ref` to a dialect's meta-schema (`https://json-schema.org/draft/2020-12/schema`, `http://json-schema.org/draft-07/schema#`, any http/https or trailing-`#` spelling) resolves from copies vendored in `lib/metaschemas.js`, so "validate this schema against its dialect" works with no registry and no network. A copy supplied through `schemas` or `addSchema()` still wins. The 2020-12 meta-schema is eight documents joined by `$dynamicRef`; see the routing change below.
|
|
19
|
+
|
|
20
|
+
- `validator.engine()` reports which engine answers `validate()` for the schema: `'codegen'`, `'closure'`, `'native'` or `'interpreter'`. A diagnostic for startup logs and benchmarks. Measured over fourteen request-shaped schemas (body, params, query, shared `$ref`, `oneOf`, `if`/`then`, local `$defs`), thirteen take the generated path; `patternProperties` with `additionalProperties: false` is the one that goes to the interpreter.
|
|
21
|
+
- `formatMode: 'inject'` for `toStandaloneModule`, `bundleStandalone`, `bundleCompact` and `build()`. The output carries no custom format source; it exports `setFormats(map)` and looks each format up from that registry at validation time, with a named error if one is missing. This is for formats that cannot be serialized: functions that close over variables, bound functions, or code rewritten by coverage and transpile steps. The default `'embed'` is unchanged in behavior, but it now checks each function's source at build time and throws with the format's name when it would not survive embedding, instead of emitting a module that fails on first use.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Draft-07 is now read by its own rules rather than as 2020-12 with renamed keywords. A schema object carrying `$ref` is that reference and nothing else, so sibling keywords, `$id` included, are ignored, as the draft specifies. A fragment-only `$id` (`"$id": "#name"`) is a plain-name anchor. A document supplied through `schemas` or `addSchema()` that declares no `$schema` of its own is read under the root's draft. JSON Pointers written against array-form `items` still resolve after the keyword is normalized to `prefixItems`. Schemas that declare no `$schema` are unaffected. Draft 7 on the official suite goes from 916 to 927 of 927.
|
|
26
|
+
- The code generator now declines, and the interpreted engine answers, whenever a document reachable through a cross-document `$ref` uses `$dynamicRef`, `$dynamicAnchor`, `unevaluatedProperties`, `unevaluatedItems`, or an embedded `$id`. Before, the generator emitted a vacuous check for such a reference and accepted everything behind it: `{ "$ref": "https://json-schema.org/draft/2020-12/schema" }` accepted `{ "type": 1 }`. These checks now live in one function, `sharedCodegenGate`, that every entry point runs first. Draft 2020-12 goes from 1294 to 1298 of 1299 and the v1 dialect from 1131 to 1133 of 1133; the one remaining 2020-12 miss needs `$vocabulary`.
|
|
27
|
+
- The interpreted engine extends the dynamic scope whenever evaluation enters a schema resource, not only when it lands on the resource's root. A `$dynamicRef` reached through `first#/$defs/stuff` now sees resource `first` in scope, which closes the last `$dynamicRef` case the interpreter missed. With code generation blocked the figures are the same as with it: 1298 of 1299, 927 of 927, 1133 of 1133.
|
|
28
|
+
- The interpreted engine is between 4 and 14 times faster depending on the schema. Each schema node is resolved once into a fixed-shape plan (type bitmask, compiled pattern, looked-up format, one flag per keyword group) and children are linked at plan time, so the walk reads no schema properties and does no map lookups. On a six-field object schema a warm `validate()` went from 3786 ns to 343 ns; a `$ref`-heavy schema from 5354 ns to 366 ns; a recursive `$dynamicRef` tree from 5302 ns to 526 ns. The compiled path is unchanged at about 44 ns on the same schema.
|
|
29
|
+
- The linear-time pattern matcher now runs a lazily built DFA over the Thompson NFA, with cached ASCII transitions and a fallback to the NFA walk past 256 states. On typical anchored patterns it is within 1.2 to 3.5 times of V8's backtracking engine (`^[0-9]{5}$` 14 ns against 12 ns, an email pattern 70 ns against 21 ns) while keeping the linear bound: `^(a+)+$` against 100,000 characters takes 1 ms. This matcher backs `pattern`, `patternProperties` and `propertyNames` in every engine and in standalone output, so all of them gain.
|
|
30
|
+
|
|
5
31
|
## 1.6.2 - 2026-08-19
|
|
6
32
|
|
|
7
33
|
### Fixed
|
package/README.md
CHANGED
|
@@ -20,11 +20,11 @@ The `ata-validator` package itself is pure JavaScript. The native accelerator (s
|
|
|
20
20
|
npm install ata-validator --omit=optional
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one,
|
|
23
|
+
or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1298 of 1299 Draft 2020-12 cases; the one case both miss needs `$vocabulary`, which ata does not implement. Only the buffer and parallel APIs (`isValid` on raw buffers, `countValid`, `batchIsValid`, `validateAndParse`) need the native engine and say so with a clear error.
|
|
24
24
|
|
|
25
|
-
Those four
|
|
25
|
+
Those four now agree with `validate()` on every case of the official suite, 3359 across three dialects. The native walker behind them does not handle every shape (`contains`, `unevaluatedProperties`, `patternProperties`, tuple `items`, cross-document `$ref`, a few formats), so for schemas using one of those the buffer APIs parse the bytes and answer through `validate()`; the list is in `lib/buffer-gate.js`. Typical request schemas stay on the zero-copy path. `npm test` holds the disagreement count at zero.
|
|
26
26
|
|
|
27
|
-
Where `new Function` is refused altogether, on Cloudflare Workers, Deno Deploy or under a strict Content-Security-Policy, ata drops to the interpreted engine and scores
|
|
27
|
+
Where `new Function` is refused altogether, on Cloudflare Workers, Deno Deploy or under a strict Content-Security-Policy, ata drops to the interpreted engine and scores the same 1298 of 1299 with code generation blocked. No flags, and on Workers no `nodejs_compat` either. See [docs/edge-runtimes.md](docs/edge-runtimes.md).
|
|
28
28
|
|
|
29
29
|
In your code:
|
|
30
30
|
|
|
@@ -65,7 +65,8 @@ offending value, a documentation link and a suggestion. On a five-field object s
|
|
|
65
65
|
passing payload costs about 17 ns and a rejected one about 155 ns. `abortEarly: true` or
|
|
66
66
|
`isValidObject()` skips that work when only the verdict matters. Schemas ata declines to
|
|
67
67
|
compile, mostly cross-document `$ref`, `$dynamicRef` and `unevaluated*`, run on the
|
|
68
|
-
interpreted engine
|
|
68
|
+
interpreted engine: about 343 ns for a passing payload on a six-field object schema, eight
|
|
69
|
+
times the compiled path, measured warm on the same machine.
|
|
69
70
|
|
|
70
71
|
## Error messages
|
|
71
72
|
|
|
@@ -496,7 +497,7 @@ Both are implemented in the interpreted engine, so a v1 schema that uses `$dynam
|
|
|
496
497
|
|
|
497
498
|
### Known limitations
|
|
498
499
|
|
|
499
|
-
Running the whole Draft 2020-12 suite with nothing excluded, `format` and `default` under specification semantics (`assertFormat: false`, `useDefaults: false`), gives
|
|
500
|
+
Running the whole Draft 2020-12 suite with nothing excluded, `format` and `default` under specification semantics (`assertFormat: false`, `useDefaults: false`), gives 1298 of 1299 cases, 99.9%. Draft 7 gives 927 of 927. The v1 dialect gives 1133 of 1133. The one 2020-12 case that fails uses a custom meta-schema that drops the validation vocabulary through `$vocabulary`, which ata does not implement, so the validation keywords still apply. `npm run test:suite` reproduces all three and names it.
|
|
500
501
|
|
|
501
502
|
Areas that remain deliberate scope decisions for 1.x:
|
|
502
503
|
|
package/build.d.ts
CHANGED
|
@@ -76,6 +76,15 @@ export function watch(opts: BuildOptions, onReport?: (r: BuildReport) => void):
|
|
|
76
76
|
export interface BundleStandaloneOptions {
|
|
77
77
|
format?: 'cjs' | 'esm';
|
|
78
78
|
formats?: Record<string, (value: string) => boolean>;
|
|
79
|
+
/**
|
|
80
|
+
* How custom format functions reach the output. 'embed' (default) writes
|
|
81
|
+
* each function's source into the module; it must be a plain function
|
|
82
|
+
* with no closed-over variables and no coverage instrumentation, or the
|
|
83
|
+
* build throws. 'inject' writes no function source: the module exports
|
|
84
|
+
* `setFormats(map)` and looks formats up from that registry at validation
|
|
85
|
+
* time, so the caller supplies them at load time.
|
|
86
|
+
*/
|
|
87
|
+
formatMode?: 'embed' | 'inject';
|
|
79
88
|
verbose?: boolean;
|
|
80
89
|
}
|
|
81
90
|
|
|
@@ -85,6 +94,9 @@ export interface ToStandaloneModuleOptions {
|
|
|
85
94
|
source?: boolean;
|
|
86
95
|
sourceMap?: unknown;
|
|
87
96
|
schemaFile?: string;
|
|
97
|
+
formats?: Record<string, (value: string) => boolean>;
|
|
98
|
+
/** See {@link BundleStandaloneOptions.formatMode}. */
|
|
99
|
+
formatMode?: 'embed' | 'inject';
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
/** Bundle multiple schemas into one self-contained module (no ata-validator runtime). */
|
package/index.d.ts
CHANGED
|
@@ -401,6 +401,13 @@ export interface Validator<T = unknown> {
|
|
|
401
401
|
|
|
402
402
|
/** Fast boolean check via JS codegen or tier 0 interpreter. No error collection. */
|
|
403
403
|
isValidObject(data: unknown): data is T;
|
|
404
|
+
/**
|
|
405
|
+
* Which engine answers `validate()` for this schema: 'codegen' (generated
|
|
406
|
+
* JS), 'closure' (the closure compiler), 'native' (the C++ engine, only
|
|
407
|
+
* for some $dynamicRef schemas) or 'interpreter'. The verdict is the same
|
|
408
|
+
* on every engine; the cost is not. A diagnostic, not a configuration.
|
|
409
|
+
*/
|
|
410
|
+
engine(): 'codegen' | 'closure' | 'native' | 'interpreter';
|
|
404
411
|
|
|
405
412
|
/** Validate a JSON string. Uses simdjson fast path for large documents. */
|
|
406
413
|
validateJSON(jsonString: string): ValidationResult<T>;
|
package/index.js
CHANGED
|
@@ -426,33 +426,35 @@ function _deepCloneWithSymbols(v) {
|
|
|
426
426
|
// Normalize a caller-provided schema without mutating the original.
|
|
427
427
|
// Clones only when normalization would change the object (draft-07 keys
|
|
428
428
|
// present or nullable fields present). Internal-only — not exported.
|
|
429
|
-
function _normalizeCallerSchema(s) {
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
s.$schema === 'http://json-schema.org/draft-07/schema'
|
|
433
|
-
|
|
429
|
+
function _normalizeCallerSchema(s, inheritDraft7) {
|
|
430
|
+
const declares = s && typeof s === 'object' && s.$schema !== undefined
|
|
431
|
+
const needsDraft7 = declares
|
|
432
|
+
? (s.$schema === 'http://json-schema.org/draft-07/schema#' || s.$schema === 'http://json-schema.org/draft-07/schema')
|
|
433
|
+
: !!inheritDraft7
|
|
434
434
|
const str = JSON.stringify(s)
|
|
435
435
|
const copy = _deepCloneWithSymbols(s)
|
|
436
|
-
if (needsDraft7) normalizeDraft7(copy)
|
|
436
|
+
if (needsDraft7) normalizeDraft7(copy, true)
|
|
437
437
|
normalizeNullable(copy)
|
|
438
438
|
// Return original when normalization produced no change, copy otherwise.
|
|
439
439
|
// Change-detection uses JSON content only; symbols do not affect it.
|
|
440
440
|
return JSON.stringify(copy) === str ? s : copy
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
-
|
|
443
|
+
// `inheritDraft7` is true when the root schema is draft-07: a retrieved
|
|
444
|
+
// document that declares no dialect is read under the root's draft.
|
|
445
|
+
function buildSchemaMap(schemas, inheritDraft7) {
|
|
444
446
|
if (!schemas) return null
|
|
445
447
|
const map = new Map()
|
|
446
448
|
if (Array.isArray(schemas)) {
|
|
447
449
|
for (const s of schemas) {
|
|
448
|
-
const normalized = _normalizeCallerSchema(s)
|
|
450
|
+
const normalized = _normalizeCallerSchema(s, inheritDraft7)
|
|
449
451
|
const id = normalized.$id
|
|
450
452
|
if (!id) throw new Error('Schema in schemas option must have $id')
|
|
451
453
|
map.set(id, normalized)
|
|
452
454
|
}
|
|
453
455
|
} else {
|
|
454
456
|
for (const [key, s] of Object.entries(schemas)) {
|
|
455
|
-
const normalized = _normalizeCallerSchema(s)
|
|
457
|
+
const normalized = _normalizeCallerSchema(s, inheritDraft7)
|
|
456
458
|
// A retrieved document is addressable both by the URI it was registered
|
|
457
459
|
// under and by the $id it declares. Registering only the $id makes
|
|
458
460
|
// references to the retrieval URI unresolvable.
|
|
@@ -560,8 +562,10 @@ class Validator {
|
|
|
560
562
|
// When schema is an object, normalization runs on a clone so the caller's
|
|
561
563
|
// object is never touched.
|
|
562
564
|
let schemaObj = typeof schema === "string"
|
|
563
|
-
? JSON.parse(schema)
|
|
565
|
+
? _normalizeCallerSchema(JSON.parse(schema))
|
|
564
566
|
: _normalizeCallerSchema(schema);
|
|
567
|
+
const rootIsDraft7 = !!(schemaObj && typeof schemaObj === 'object' && typeof schemaObj.$schema === 'string' &&
|
|
568
|
+
(schemaObj.$schema === 'http://json-schema.org/draft-07/schema#' || schemaObj.$schema === 'http://json-schema.org/draft-07/schema'));
|
|
565
569
|
|
|
566
570
|
// assertFormat: false makes `format` annotation-only. Strip it on a clone
|
|
567
571
|
// so the caller's schema keeps the keyword.
|
|
@@ -579,11 +583,12 @@ class Validator {
|
|
|
579
583
|
this._compiled = null;
|
|
580
584
|
this._fastSlot = -1;
|
|
581
585
|
this._jsFn = null;
|
|
586
|
+
this._engine = undefined;
|
|
582
587
|
this._preprocess = null;
|
|
583
588
|
this._applyDefaults = null;
|
|
584
589
|
|
|
585
590
|
// Schema map for cross-schema $ref resolution
|
|
586
|
-
this._schemaMap = buildSchemaMap(options.schemas) || new Map();
|
|
591
|
+
this._schemaMap = buildSchemaMap(options.schemas, rootIsDraft7) || new Map();
|
|
587
592
|
|
|
588
593
|
// User-supplied format checkers: { formatName: (value) => boolean }.
|
|
589
594
|
// Looked up at runtime when a schema references a format the built-in
|
|
@@ -717,6 +722,20 @@ class Validator {
|
|
|
717
722
|
// Lazy stringify — only computed here, not in constructor
|
|
718
723
|
if (!this._schemaStr) this._schemaStr = JSON.stringify(schemaObj);
|
|
719
724
|
|
|
725
|
+
// A $ref to a meta-schema resolves from the vendored copies, so
|
|
726
|
+
// "validate this schema against its dialect" needs no network and no
|
|
727
|
+
// caller-supplied registry. Only schemas that mention json-schema.org in a
|
|
728
|
+
// reference pay for the lookup.
|
|
729
|
+
if (this._schemaStr.includes('json-schema.org/draft')) {
|
|
730
|
+
const { METASCHEMAS } = require('./lib/metaschemas');
|
|
731
|
+
for (const [id, meta] of METASCHEMAS) {
|
|
732
|
+
const bare = id.replace(/#$/, '');
|
|
733
|
+
for (const key of [id, bare, bare + '#', bare.replace(/^https:/, 'http:'), bare.replace(/^http:/, 'https:')]) {
|
|
734
|
+
if (!this._schemaMap.has(key)) this._schemaMap.set(key, meta);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
720
739
|
// Check cache first -- reuse compiled functions for same schema
|
|
721
740
|
const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
|
|
722
741
|
const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
|
|
@@ -754,6 +773,7 @@ class Validator {
|
|
|
754
773
|
jsCombinedFn = compileToJSCombined(schemaObj, VALID_RESULT, sm, uf);
|
|
755
774
|
jsErrFn = compileToJSCodegenWithErrors(schemaObj, sm, uf);
|
|
756
775
|
_isCodegen = !!_cgFn;
|
|
776
|
+
this._engine = _cgFn ? 'codegen' : jsFn ? 'closure' : null;
|
|
757
777
|
if (!uf) {
|
|
758
778
|
_compileCache.set(mapKey, { jsFn, combined: jsCombinedFn, errFn: jsErrFn, isCodegen: _isCodegen });
|
|
759
779
|
}
|
|
@@ -761,6 +781,7 @@ class Validator {
|
|
|
761
781
|
jsFn = null; jsCombinedFn = null; jsErrFn = null;
|
|
762
782
|
}
|
|
763
783
|
this._jsFn = jsFn;
|
|
784
|
+
if (this._engine === undefined) this._engine = cached ? (cached.isCodegen ? 'codegen' : jsFn ? 'closure' : null) : null;
|
|
764
785
|
|
|
765
786
|
// Data mutators -- try codegen first (12x faster), fallback to closure arrays.
|
|
766
787
|
// Follow cross-refs so coercion/defaults/removeAdditional see the referenced
|
|
@@ -1081,6 +1102,7 @@ class Validator {
|
|
|
1081
1102
|
if (_hasDynRef && !_hasUneval && !_hasPropDeps && !this._v1Dynamic) {
|
|
1082
1103
|
// validateJSON is the C++ path with full anchor-map support; the NAPI
|
|
1083
1104
|
// direct V8 `validate` path has no anchor maps.
|
|
1105
|
+
this._engine = 'native';
|
|
1084
1106
|
_validate = (data) => this._compiled.validateJSON(JSON.stringify(data));
|
|
1085
1107
|
this.validateJSON = (jsonStr) => this._compiled.validateJSON(jsonStr);
|
|
1086
1108
|
this.isValidJSON = (jsonStr) => this._compiled.isValidJSON(jsonStr);
|
|
@@ -1091,6 +1113,7 @@ class Validator {
|
|
|
1091
1113
|
formats: this._userFormats,
|
|
1092
1114
|
v1: isV1Dialect(schemaObj),
|
|
1093
1115
|
});
|
|
1116
|
+
this._engine = 'interpreter';
|
|
1094
1117
|
_validate = (data) => interp.validate(data);
|
|
1095
1118
|
this.validateJSON = (jsonStr) => {
|
|
1096
1119
|
try {
|
|
@@ -1149,6 +1172,7 @@ class Validator {
|
|
|
1149
1172
|
formats: this._userFormats,
|
|
1150
1173
|
v1: isV1Dialect(schemaObj),
|
|
1151
1174
|
});
|
|
1175
|
+
this._engine = 'interpreter';
|
|
1152
1176
|
const run = preprocess
|
|
1153
1177
|
? (data) => { preprocess(data); return interp.validate(data); }
|
|
1154
1178
|
: (data) => interp.validate(data);
|
|
@@ -1311,12 +1335,29 @@ class Validator {
|
|
|
1311
1335
|
}
|
|
1312
1336
|
}
|
|
1313
1337
|
|
|
1338
|
+
// The buffer APIs answer from the native walker, which disagrees with
|
|
1339
|
+
// validate() on shapes listed in lib/buffer-gate.js. For those schemas
|
|
1340
|
+
// every buffer entry point goes through validate() instead.
|
|
1341
|
+
if (native) {
|
|
1342
|
+
const { bufferNeedsSlowPath, installSlowBufferApis } = require('./lib/buffer-gate');
|
|
1343
|
+
if (bufferNeedsSlowPath(schemaObj, this._schemaMap)) installSlowBufferApis(this);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1314
1346
|
// Save to identity cache for ultra-fast reuse with same schema object
|
|
1315
1347
|
if (this._schemaObj && typeof this._schemaObj === 'object') {
|
|
1316
1348
|
_identityCache.set(this._schemaObj, this);
|
|
1317
1349
|
}
|
|
1318
1350
|
}
|
|
1319
1351
|
|
|
1352
|
+
// Which engine answers validate() for this schema: 'codegen' (generated
|
|
1353
|
+
// JS), 'closure' (the closure compiler, the boolean fallback), 'native'
|
|
1354
|
+
// (the C++ engine, only for some $dynamicRef schemas), or 'interpreter'.
|
|
1355
|
+
// A diagnostic: the answer is the same on every engine, the cost is not.
|
|
1356
|
+
engine() {
|
|
1357
|
+
this._ensureCompiled();
|
|
1358
|
+
return this._engine || 'interpreter';
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1320
1361
|
_ensureNative() {
|
|
1321
1362
|
if (this._nativeReady) return;
|
|
1322
1363
|
this._nativeReady = true;
|
|
@@ -1341,8 +1382,12 @@ class Validator {
|
|
|
1341
1382
|
if (!schema || !schema.$id) {
|
|
1342
1383
|
throw new Error('Schema must have $id')
|
|
1343
1384
|
}
|
|
1344
|
-
// Normalize a copy so the caller's object is never mutated.
|
|
1345
|
-
|
|
1385
|
+
// Normalize a copy so the caller's object is never mutated. A document
|
|
1386
|
+
// without a dialect of its own is read under the root's draft.
|
|
1387
|
+
const root = this._schemaObj
|
|
1388
|
+
const rootIsDraft7 = !!(root && typeof root === 'object' && typeof root.$schema === 'string' &&
|
|
1389
|
+
(root.$schema === 'http://json-schema.org/draft-07/schema#' || root.$schema === 'http://json-schema.org/draft-07/schema'))
|
|
1390
|
+
const normalized = _normalizeCallerSchema(schema, rootIsDraft7)
|
|
1346
1391
|
this._schemaMap.set(normalized.$id, normalized)
|
|
1347
1392
|
}
|
|
1348
1393
|
|
package/lib/aot-build.js
CHANGED
package/lib/aot.js
CHANGED
|
@@ -98,6 +98,48 @@ module.exports = { boolFn, hybridFactory, errFn };
|
|
|
98
98
|
//
|
|
99
99
|
// format: 'esm' | 'cjs'. Default 'esm'.
|
|
100
100
|
// abortEarly: if true, invalid result is a shared stub; smaller output.
|
|
101
|
+
|
|
102
|
+
// Custom format functions in standalone output.
|
|
103
|
+
//
|
|
104
|
+
// 'embed' (default) writes each function's source into the module through
|
|
105
|
+
// Function#toString. That only works for a plain function: one that closes
|
|
106
|
+
// over nothing and is not rewritten by a coverage or transpile step (istanbul
|
|
107
|
+
// injects `cov_` counters; a bundler may hoist helpers). The check below
|
|
108
|
+
// rejects those at build time with a named error instead of emitting a module
|
|
109
|
+
// that throws on first use.
|
|
110
|
+
//
|
|
111
|
+
// 'inject' writes no function source. The module exports `setFormats(map)`
|
|
112
|
+
// and each format is looked up from that registry when a value is checked,
|
|
113
|
+
// so the caller supplies the functions at load time.
|
|
114
|
+
function emitFormatDecls(closures, mode, declKW) {
|
|
115
|
+
if (!closures || closures.length === 0) return { decls: '', exportsSetFormats: false };
|
|
116
|
+
if (mode === 'inject') {
|
|
117
|
+
let out = `${declKW} __formats = Object.create(null);\n`;
|
|
118
|
+
out += `function setFormats(map) { for (const k in map) __formats[k] = map[k]; }\n`;
|
|
119
|
+
for (const { name, format } of closures) {
|
|
120
|
+
const key = JSON.stringify(format === null ? name.slice(4) : format);
|
|
121
|
+
out += `${declKW} ${name} = function (v) { const f = __formats[${key}]; if (typeof f !== 'function') throw new Error('ata: format ' + ${key} + ' is not registered; call setFormats({ [' + ${key} + ']: fn }) before validating'); return f(v); };\n`;
|
|
122
|
+
}
|
|
123
|
+
return { decls: out, exportsSetFormats: true };
|
|
124
|
+
}
|
|
125
|
+
let out = '';
|
|
126
|
+
for (const { name, fn, format } of closures) {
|
|
127
|
+
const src = fn.toString();
|
|
128
|
+
const label = format === null ? name : format;
|
|
129
|
+
if (/\bcov_[A-Za-z0-9_$]+\b/.test(src)) {
|
|
130
|
+
throw new Error(`ata: custom format "${label}" is instrumented for coverage and cannot be embedded; use { formatMode: 'inject' } and register it with setFormats() at load time`);
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
// eslint-disable-next-line no-new-func
|
|
134
|
+
new Function('return (' + src + ')');
|
|
135
|
+
} catch {
|
|
136
|
+
throw new Error(`ata: custom format "${label}" has no standalone source (a bound function, a class method, or a native); use { formatMode: 'inject' } and register it with setFormats() at load time`);
|
|
137
|
+
}
|
|
138
|
+
out += `${declKW} ${name} = ${src};\n`;
|
|
139
|
+
}
|
|
140
|
+
return { decls: out, exportsSetFormats: false };
|
|
141
|
+
}
|
|
142
|
+
|
|
101
143
|
function toStandaloneModule(validator, opts) {
|
|
102
144
|
validator._ensureCompiled();
|
|
103
145
|
const jsFn = validator._jsFn;
|
|
@@ -170,20 +212,17 @@ function toStandaloneModule(validator, opts) {
|
|
|
170
212
|
// User-supplied format functions are referenced as _uf_<name> by both the
|
|
171
213
|
// boolean (_fn) and error (errFn) bodies. Embed them via Function#toString
|
|
172
214
|
// so the standalone module stays self-contained.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
formatDecls = jsFn._formatClosures
|
|
176
|
-
.map(({ name, fn }) => `const ${name} = ${fn.toString()};`)
|
|
177
|
-
.join('\n') + '\n';
|
|
178
|
-
}
|
|
215
|
+
const fmt = emitFormatDecls(jsFn._formatClosures, opts && opts.formatMode, 'const');
|
|
216
|
+
const formatDecls = fmt.decls;
|
|
179
217
|
|
|
180
218
|
const validBody = errCore
|
|
181
219
|
? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
|
|
182
220
|
: 'return _fn(data) ? VALID : ABORT';
|
|
183
221
|
|
|
222
|
+
const names = fmt.exportsSetFormats ? 'validate, isValid, setFormats' : 'validate, isValid';
|
|
184
223
|
const exports = format === 'esm'
|
|
185
|
-
? `export {
|
|
186
|
-
: `module.exports = {
|
|
224
|
+
? `export { ${names} };\nexport default { ${names} };\n`
|
|
225
|
+
: `module.exports = { ${names} };\nmodule.exports.default = module.exports;\n`;
|
|
187
226
|
|
|
188
227
|
return `// Auto-generated by ata-validator — do not edit.
|
|
189
228
|
// Schema is embedded; runtime has zero dependency on ata-validator.
|
|
@@ -240,6 +279,7 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
240
279
|
const format = (opts && opts.format) || 'cjs';
|
|
241
280
|
const R = 'Object.freeze({valid:true,errors:Object.freeze([])})';
|
|
242
281
|
let bundleUsesSafeRe = false;
|
|
282
|
+
let bundleInjects = false;
|
|
243
283
|
const fns = schemas.map((schema) => {
|
|
244
284
|
const v = new Validator(schema, bundleOpts);
|
|
245
285
|
v._ensureCompiled();
|
|
@@ -255,12 +295,13 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
255
295
|
jsErrFn && jsErrFn._errSource
|
|
256
296
|
? jsErrFn._errSource
|
|
257
297
|
: "return{valid:false,errors:[{code:'error',path:'',message:'validation failed'}]}";
|
|
258
|
-
//
|
|
298
|
+
// Custom format closures: embedded, or bound to the bundle-level
|
|
299
|
+
// registry when opts.formats is 'inject'.
|
|
259
300
|
let preamble = '';
|
|
260
301
|
if (jsFn._formatClosures) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
302
|
+
const f = emitFormatDecls(jsFn._formatClosures, opts && opts.formatMode, 'var');
|
|
303
|
+
preamble = f.decls.replace(/^var __formats = [^\n]*\n|^function setFormats[^\n]*\n/gm, '');
|
|
304
|
+
if (f.exportsSetFormats) bundleInjects = true;
|
|
264
305
|
}
|
|
265
306
|
// Include hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) so the
|
|
266
307
|
// bundle output is self-contained. `toStandalone` emits this same source
|
|
@@ -277,10 +318,15 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
277
318
|
});
|
|
278
319
|
const arr = `[${fns.join(',')}]`;
|
|
279
320
|
const safeEmbed = bundleUsesSafeRe ? getSafeRegexEmbed() + '\n' : '';
|
|
321
|
+
const registry = bundleInjects
|
|
322
|
+
? `var __formats=Object.create(null);\nfunction setFormats(map){for(var k in map)__formats[k]=map[k]}\n`
|
|
323
|
+
: '';
|
|
280
324
|
if (format === 'esm') {
|
|
281
|
-
|
|
325
|
+
const extra = bundleInjects ? 'export { validators, setFormats };' : 'export { validators };';
|
|
326
|
+
return `// Auto-generated by ata-validator — do not edit\n${safeEmbed}${registry}const R=${R};\nconst validators=${arr};\nexport default validators;\n${extra}\n`;
|
|
282
327
|
}
|
|
283
|
-
|
|
328
|
+
const attach = bundleInjects ? 'module.exports.setFormats=setFormats;\n' : '';
|
|
329
|
+
return `'use strict';\n${safeEmbed}${registry}var R=${R};\nmodule.exports=[${fns.join(',')}];\n${attach}`;
|
|
284
330
|
}
|
|
285
331
|
|
|
286
332
|
// Compact bundle: deduplicated code. Shared template functions + per-schema params.
|
|
@@ -355,14 +401,17 @@ function bundleCompact(Validator, schemas, opts) {
|
|
|
355
401
|
// bodies. Collect them across all schemas (deduped by name) and embed via
|
|
356
402
|
// Function#toString so the bundle stays self-contained.
|
|
357
403
|
const fmtSeen = new Set();
|
|
404
|
+
const fmtAll = [];
|
|
358
405
|
for (const e of entries) {
|
|
359
406
|
if (!e || !e.fmt) continue;
|
|
360
|
-
for (const
|
|
361
|
-
if (fmtSeen.has(name)) continue;
|
|
362
|
-
fmtSeen.add(name);
|
|
363
|
-
|
|
407
|
+
for (const entry of e.fmt) {
|
|
408
|
+
if (fmtSeen.has(entry.name)) continue;
|
|
409
|
+
fmtSeen.add(entry.name);
|
|
410
|
+
fmtAll.push(entry);
|
|
364
411
|
}
|
|
365
412
|
}
|
|
413
|
+
const fmtOut = emitFormatDecls(fmtAll, opts && opts.formatMode, declKW);
|
|
414
|
+
out += fmtOut.decls;
|
|
366
415
|
|
|
367
416
|
// Shared hybrid factories
|
|
368
417
|
out += `${declKW} H=[\n`;
|
|
@@ -385,9 +434,10 @@ function bundleCompact(Validator, schemas, opts) {
|
|
|
385
434
|
})
|
|
386
435
|
.join(',');
|
|
387
436
|
if (isEsm) {
|
|
388
|
-
out += `const validators=[${arrBody}];\nexport default validators;\nexport { validators };\n`;
|
|
437
|
+
out += `const validators=[${arrBody}];\nexport default validators;\nexport { validators${fmtOut.exportsSetFormats ? ', setFormats' : ''} };\n`;
|
|
389
438
|
} else {
|
|
390
439
|
out += `module.exports=[${arrBody}];\n`;
|
|
440
|
+
if (fmtOut.exportsSetFormats) out += 'module.exports.setFormats=setFormats;\n';
|
|
391
441
|
}
|
|
392
442
|
|
|
393
443
|
return out;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The buffer APIs (isValid, countValid, batchIsValid, isValidNDJSON,
|
|
4
|
+
// isValidParallel, isValidPrepadded, and isValidJSON above the simdjson
|
|
5
|
+
// threshold) answer from the native engine's own walker, which is not the
|
|
6
|
+
// engine validate() uses. Measured over the official suite, the two disagreed
|
|
7
|
+
// on 245 of 2222 cases, and 195 of those were the native walker accepting a
|
|
8
|
+
// document validate() rejects. A buffer API that is faster and sometimes
|
|
9
|
+
// wrong in the accepting direction is not a fast path, it is a hole.
|
|
10
|
+
//
|
|
11
|
+
// This module decides, once per schema, whether the native walker can be
|
|
12
|
+
// trusted for it. When it cannot, the buffer APIs are replaced with versions
|
|
13
|
+
// that parse the bytes and call validate(), so every entry point gives the
|
|
14
|
+
// same verdict. The list below is the set of shapes where the suite showed
|
|
15
|
+
// the walker disagreeing, each with the reason. It is deliberately a list of
|
|
16
|
+
// shapes rather than a count: a new disagreement is a new entry here, with
|
|
17
|
+
// tests/test_buffer_path_parity.js holding the total at zero.
|
|
18
|
+
|
|
19
|
+
// Keywords the native walker does not implement, or implements differently
|
|
20
|
+
// enough to disagree on the suite.
|
|
21
|
+
const UNSUPPORTED_KEYWORDS = new Set([
|
|
22
|
+
'contains', 'minContains', 'maxContains',
|
|
23
|
+
'unevaluatedProperties', 'unevaluatedItems',
|
|
24
|
+
'dependencies', 'dependentSchemas', 'dependentRequired',
|
|
25
|
+
'propertyNames', 'patternProperties',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
// Formats the native checkers answer differently from the JS ones.
|
|
29
|
+
const DIVERGENT_FORMATS = new Set(['hostname', 'date-time', 'time', 'uri-reference', 'duration']);
|
|
30
|
+
|
|
31
|
+
const SUBSCHEMA_MAPS = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas', 'dependencies'];
|
|
32
|
+
const SUBSCHEMA_NODES = [
|
|
33
|
+
'items', 'additionalItems', 'additionalProperties', 'contains', 'propertyNames', 'not',
|
|
34
|
+
'if', 'then', 'else', 'unevaluatedProperties', 'unevaluatedItems',
|
|
35
|
+
'allOf', 'anyOf', 'oneOf', 'prefixItems',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function walk(schema, depth) {
|
|
39
|
+
if (schema === true || schema === false) {
|
|
40
|
+
// A boolean root is answered wrong by the walker; nested booleans are
|
|
41
|
+
// handled where they appear (items and prefixItems below).
|
|
42
|
+
return depth === 0;
|
|
43
|
+
}
|
|
44
|
+
if (schema === null || typeof schema !== 'object') return false;
|
|
45
|
+
if (Array.isArray(schema)) {
|
|
46
|
+
for (const s of schema) if (walk(s, depth + 1)) return true;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
for (const key of Object.keys(schema)) {
|
|
50
|
+
const v = schema[key];
|
|
51
|
+
if (UNSUPPORTED_KEYWORDS.has(key)) return true;
|
|
52
|
+
// Cross-document references: the native engine has no registry of
|
|
53
|
+
// external schemas, and embedded $id changes the base URI in ways its
|
|
54
|
+
// resolver gets wrong.
|
|
55
|
+
if (key === '$ref' && typeof v === 'string' && !v.startsWith('#')) return true;
|
|
56
|
+
if (key === '$id' && depth > 0) return true;
|
|
57
|
+
// An empty enum rejects everything; the walker accepts everything.
|
|
58
|
+
if (key === 'enum' && Array.isArray(v) && v.length === 0) return true;
|
|
59
|
+
if (key === 'format' && DIVERGENT_FORMATS.has(v)) return true;
|
|
60
|
+
// Unicode property escapes: RE2 cannot parse them and the walker then
|
|
61
|
+
// skips the pattern instead of failing.
|
|
62
|
+
if (key === 'pattern' && typeof v === 'string' && /\\[pP]\{/.test(v)) return true;
|
|
63
|
+
// Tuple forms: prefixItems and the draft-07 array form of items are
|
|
64
|
+
// checked against the wrong positions by the walker.
|
|
65
|
+
if (key === 'prefixItems') return true;
|
|
66
|
+
if ((key === 'items' || key === 'additionalItems') && (typeof v === 'boolean' || Array.isArray(v))) return true;
|
|
67
|
+
|
|
68
|
+
if (SUBSCHEMA_MAPS.includes(key)) {
|
|
69
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
70
|
+
for (const k of Object.keys(v)) if (walk(v[k], depth + 1)) return true;
|
|
71
|
+
}
|
|
72
|
+
} else if (SUBSCHEMA_NODES.includes(key)) {
|
|
73
|
+
if (walk(v, depth + 1)) return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// True when the buffer APIs must answer through validate() for this schema.
|
|
80
|
+
function bufferNeedsSlowPath(schema, schemaMap) {
|
|
81
|
+
if (walk(schema, 0)) return true;
|
|
82
|
+
if (schemaMap && schemaMap.size > 0) {
|
|
83
|
+
for (const s of schemaMap.values()) if (walk(s, 1)) return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toText(input, name) {
|
|
89
|
+
if (typeof input === 'string') return input;
|
|
90
|
+
if (input instanceof Uint8Array) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString('utf8');
|
|
91
|
+
throw new TypeError(`${name}() requires a Buffer, Uint8Array, or string. For parsed objects, use isValidObject().`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Replaces the instance's buffer APIs with versions that parse and call
|
|
95
|
+
// validate(). Installed after compilation, so `validator.validate` is final.
|
|
96
|
+
function installSlowBufferApis(validator) {
|
|
97
|
+
const isValidText = (text) => {
|
|
98
|
+
let value;
|
|
99
|
+
try { value = JSON.parse(text); } catch { return false; }
|
|
100
|
+
return validator.validate(value).valid;
|
|
101
|
+
};
|
|
102
|
+
validator.isValid = (input) => isValidText(toText(input, 'isValid'));
|
|
103
|
+
validator.isValidJSON = (jsonStr) => isValidText(jsonStr);
|
|
104
|
+
validator.isValidPrepadded = (paddedBuffer, jsonLength) =>
|
|
105
|
+
isValidText(Buffer.from(paddedBuffer.buffer, paddedBuffer.byteOffset, jsonLength).toString('utf8'));
|
|
106
|
+
const ndjson = (input, name) => {
|
|
107
|
+
const lines = toText(input, name).split('\n');
|
|
108
|
+
const out = [];
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
if (line === '') continue; // the native loop skips zero-length lines only
|
|
111
|
+
out.push(isValidText(line));
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
};
|
|
115
|
+
validator.isValidNDJSON = (input) => ndjson(input, 'isValidNDJSON');
|
|
116
|
+
validator.isValidParallel = (input) => ndjson(input, 'isValidParallel');
|
|
117
|
+
validator.countValid = (input) => {
|
|
118
|
+
let n = 0;
|
|
119
|
+
for (const ok of ndjson(input, 'countValid')) if (ok) n++;
|
|
120
|
+
return n;
|
|
121
|
+
};
|
|
122
|
+
validator.batchIsValid = (buffers) => {
|
|
123
|
+
let n = 0;
|
|
124
|
+
for (const b of buffers) {
|
|
125
|
+
if (!(b instanceof Uint8Array)) throw new TypeError('batchIsValid() requires Buffer or Uint8Array elements');
|
|
126
|
+
if (isValidText(toText(b, 'batchIsValid'))) n++;
|
|
127
|
+
}
|
|
128
|
+
return n;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { bufferNeedsSlowPath, installSlowBufferApis };
|
package/lib/draft7.js
CHANGED
|
@@ -9,15 +9,37 @@ function isDraft7(schema) {
|
|
|
9
9
|
return !!(schema && schema.$schema && DRAFT7_SCHEMAS.has(schema.$schema))
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// `force` applies the draft-07 rules to a document that declares no dialect
|
|
13
|
+
// of its own, which is how a retrieved schema inherits the root's draft.
|
|
14
|
+
function normalizeDraft7(schema, force) {
|
|
15
|
+
if (!force && !isDraft7(schema)) return schema
|
|
14
16
|
_normalize(schema)
|
|
15
17
|
return schema
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
// Keywords that may sit next to `$ref` in draft-07 without being applied:
|
|
21
|
+
// definitions are addressable by pointer, annotations are inert.
|
|
22
|
+
const REF_SIBLINGS_KEPT = new Set(['$ref', '$defs', 'definitions', '$schema', '$comment', 'title', 'description', 'examples', 'default', 'readOnly', 'writeOnly'])
|
|
23
|
+
|
|
18
24
|
function _normalize(schema) {
|
|
19
25
|
if (typeof schema !== 'object' || schema === null) return
|
|
20
26
|
|
|
27
|
+
// In draft-07 an object with `$ref` is that reference and nothing else:
|
|
28
|
+
// sibling keywords, `$id` included, are ignored. Dropping them here gives
|
|
29
|
+
// every engine the same reading without each having to know the draft.
|
|
30
|
+
if (typeof schema.$ref === 'string') {
|
|
31
|
+
for (const key of Object.keys(schema)) {
|
|
32
|
+
if (!REF_SIBLINGS_KEPT.has(key)) delete schema[key]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// A fragment-only `$id` is a plain-name anchor in draft-07; 2020-12 spells
|
|
37
|
+
// it `$anchor`, which every engine already resolves.
|
|
38
|
+
if (typeof schema.$id === 'string' && /^#[A-Za-z][A-Za-z0-9_.:-]*$/.test(schema.$id)) {
|
|
39
|
+
if (schema.$anchor === undefined) schema.$anchor = schema.$id.slice(1)
|
|
40
|
+
delete schema.$id
|
|
41
|
+
}
|
|
42
|
+
|
|
21
43
|
// definitions → $defs
|
|
22
44
|
if (schema.definitions && !schema.$defs) {
|
|
23
45
|
schema.$defs = schema.definitions
|