ata-validator 1.48.0 → 1.49.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
@@ -109,6 +109,29 @@ file.
109
109
  passes with nothing failed, errored or skipped under Bowtie's own runner. The v1 dialect is
110
110
  declared in a pending harness change that waits for a Bowtie release.
111
111
 
112
+ ### Side by side
113
+
114
+ [validator-benchmarks](https://github.com/ata-core/validator-benchmarks) is ata's own
115
+ comparison, so it comes after the ones above: eleven validators on three realistic bodies
116
+ and four scenes, each library in its own process, 27 samples per cell, every schema and
117
+ document in the repository. On an order body with formats and a pattern, 900 valid
118
+ documents, Node 24:
119
+
120
+ [![Documents a second on one order body, eleven validators](https://raw.githubusercontent.com/ata-core/validator-benchmarks/main/media/race-order-verdict-valid.gif)](https://github.com/ata-core/validator-benchmarks/blob/main/media/race-order-verdict-valid.mp4)
121
+
122
+ | | documents a second, the verdict | same, `new Function` refused |
123
+ |---|---:|---:|
124
+ | ata | 3,295,880 | 607,614 |
125
+ | ata, compiled module | 3,319,594 | 3,344,948 |
126
+ | typebox | 1,638,813 | EvalError |
127
+ | ajv | 1,285,492 | EvalError |
128
+ | typia | 939,572 | 964,507 |
129
+ | zod | 440,473 | 237,176 |
130
+
131
+ Where ata is behind is in the same tables: on a four-field body that fails, ajv answers the
132
+ verdict faster, and with every error read it does 6.6M a second against ata's 2.9M with rich
133
+ errors (7.6M with `richErrors: false`).
134
+
112
135
  ## Why AOT
113
136
 
114
137
  | Dimension | Schema | ata-AOT | runtime validator | Difference |
@@ -254,13 +277,13 @@ is the most common way to get a misleading number out of this library.
254
277
 
255
278
  | | compiled with `ata build` | runtime `new Validator(schema)` |
256
279
  |---|---|---|
257
- | In a bundle, gzipped | **2.1 KB** | 125.0 KB |
280
+ | In a bundle, gzipped | **2.1 KB** | 126.1 KB |
258
281
  | Time to a served request | **3.5 ms** | 7.6 ms |
259
282
  | Schema known when | build time | any time |
260
283
 
261
284
  The bundle row is the ten-field user schema in
262
285
  `tests/fixtures/error-dx/user.schema.json`, every export of the compiled module against
263
- `new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.47.0.
286
+ `new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.48.0.
264
287
  The startup row is a Hono route on Bun 1.4, the median of three rounds of best-of-seven, from
265
288
  `benchmark/bundle`, against 3.5 ms for the same app doing no validation at all, so the
266
289
  compiled path costs nothing measurable to start. The runtime figure is what it is because
@@ -703,7 +726,7 @@ Copy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See
703
726
 
704
727
  ### Dialects
705
728
 
706
- `$schema` selects the dialect. Draft 2020-12 is the default, draft-07 is normalized on the way in, and `https://json-schema.org/v1` (or the dated `https://json-schema.org/v1/2026`) selects JSON Schema v1.
729
+ `$schema` selects the dialect. Draft 2020-12 is the default; draft-07, draft-06 and Draft 2019-09 are rewritten to the 2020-12 spelling on the way in (draft-06 as draft-07 without `if`/`then`/`else`, 2019-09's tuple `items` and `$recursiveRef` as `prefixItems` and `$dynamicRef`); and `https://json-schema.org/v1` (or the dated `https://json-schema.org/v1/2026`) selects JSON Schema v1. The official suite passes in full on all five: 1301 of 1301 on 2020-12, 1261 of 1261 on 2019-09, 929 of 929 on draft-07, 841 of 841 on draft-06 and 1135 of 1135 on v1, with code generation allowed and blocked. Draft-04 and earlier are not supported; draft-04's boolean `exclusiveMinimum`/`exclusiveMaximum` is read wherever it appears.
707
730
 
708
731
  Two things differ under v1. `propertyDependencies` selects a subschema by the value of a property rather than by its presence, which is what `dependentSchemas` does. And `$dynamicRef` no longer requires bookending: the reference resolves through the dynamic scope whether or not the schema it first lands on carries a matching `$dynamicAnchor`, so the outermost matching anchor still in scope wins. Everything else ata implements is identical under both dialects, so a schema that declares no `$schema` behaves exactly as before.
709
732
 
package/index.js CHANGED
@@ -814,22 +814,40 @@ function hasDefaultsInside(node, seen) {
814
814
  function emitDefaults(node, ov, lines, st) {
815
815
  if (st.seen.has(node)) { st.cycle = true; return; }
816
816
  st.seen.add(node);
817
+ // A default is written only where the key is not an own property, and the
818
+ // own-property test used to be Object.hasOwn for every default on every
819
+ // call: on a configuration schema of 82 defaults that was 820 ns a call
820
+ // for a document that already carried every key, against 12 ns with the
821
+ // pass off. For a plain object (prototype Object.prototype) a key the
822
+ // prototype does not have is own exactly when reading it gives a value, so
823
+ // the read, an inline-cached load, settles the common case and hasOwn is
824
+ // consulted only when the read gives undefined. Keys Object.prototype does
825
+ // have (constructor, toString, __proto__) keep the hasOwn test, since the
826
+ // read would find the inherited one; so does any object with another
827
+ // prototype, where a read may find an inherited value of the same name.
828
+ const pk = '_pk' + st.n++;
829
+ lines.push(`const ${pk}=Object.getPrototypeOf(${ov})===Object.prototype`);
817
830
  for (const [key, prop] of Object.entries(node.properties || {})) {
818
831
  if (!prop || typeof prop !== 'object') continue;
819
832
  const k = JSON.stringify(key);
833
+ const inherited = key in Object.prototype;
834
+ const absent = inherited ? `!Object.hasOwn(${ov},${k})` : `(${ov}[${k}]===undefined||!${pk})&&!Object.hasOwn(${ov},${k})`;
820
835
  if (prop.default !== undefined) {
821
836
  const def = JSON.stringify(prop.default);
822
837
  // Assignment to a key named __proto__ hits the prototype setter
823
838
  // instead of creating a property; defineProperty writes an own key.
824
839
  lines.push(key === '__proto__'
825
- ? `if(!Object.hasOwn(${ov},${k}))Object.defineProperty(${ov},${k},{value:${def},writable:true,enumerable:true,configurable:true})`
826
- : `if(!Object.hasOwn(${ov},${k}))${ov}[${k}]=${def}`);
840
+ ? `if(${absent})Object.defineProperty(${ov},${k},{value:${def},writable:true,enumerable:true,configurable:true})`
841
+ : `if(${absent})${ov}[${k}]=${def}`);
827
842
  }
828
843
  // Into an own property that holds an object, arrays included, as the
829
- // closure pass walks it.
844
+ // closure pass walks it. Object.prototype's own properties are functions
845
+ // apart from the __proto__ accessor, so on a plain object a read that
846
+ // gives an object is an own property for every other key.
830
847
  if (prop.properties && hasDefaultsInside(prop, new Set())) {
831
848
  const n = '_d' + st.n++;
832
- lines.push(`if(Object.hasOwn(${ov},${k})){const ${n}=${ov}[${k}];if(typeof ${n}==='object'&&${n}!==null){`);
849
+ const own = inherited ? `Object.hasOwn(${ov},${k})` : `(${pk}||Object.hasOwn(${ov},${k}))`;
850
+ lines.push(`{const ${n}=${ov}[${k}];if(typeof ${n}==='object'&&${n}!==null&&${own}){`);
833
851
  emitDefaults(prop, n, lines, st);
834
852
  lines.push('}}');
835
853
  }
package/lib/aot-impl.js CHANGED
@@ -113,6 +113,9 @@ function toStandalone(validator) {
113
113
  // Also capture error function source for zero-compile standalone load
114
114
  const jsErrFn = compileToJSCodegenWithErrors(
115
115
  typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
116
+ null,
117
+ null,
118
+ { noOrdinal: true },
116
119
  );
117
120
  const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
118
121
 
@@ -388,11 +391,14 @@ function toStandaloneModule(validator, opts) {
388
391
  `const _ER = function(e) { _ataStamp(e); this.valid = false; this.errors = e; };\n` +
389
392
  `${codeTableSource(onePass.keywords, onePass.formats)}const _vC = (function() {\n${onePass.core}\n})();\n`;
390
393
  } else if (!abortEarly) {
394
+ // A module returns the collector's errors as they are and never sorts
395
+ // them, so the ordinal the runtime sorts by (`_o`) has no reader there;
396
+ // it leaked into every error object a module reported.
391
397
  jsErrFn = compileToJSCodegenWithErrors(
392
398
  typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
393
399
  null,
394
400
  validator._userFormats,
395
- (source && sourceMap && schemaFile) ? { sourceMap, schemaFile, frames: sourceFrames } : null,
401
+ (source && sourceMap && schemaFile) ? { sourceMap, schemaFile, frames: sourceFrames, noOrdinal: true } : { noOrdinal: true },
396
402
  );
397
403
  const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
398
404
  if (errSrc && jsErrFn._errFactory) {
@@ -665,6 +671,7 @@ function bundleStandalone(Validator, schemas, opts) {
665
671
  v._schemaObj,
666
672
  v._schemaMap,
667
673
  v._userFormats,
674
+ { noOrdinal: true },
668
675
  );
669
676
  if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
670
677
  return { v, jsFn, jsErrFn };
@@ -757,6 +764,7 @@ function bundleCompact(Validator, schemas, opts) {
757
764
  v._schemaObj,
758
765
  v._schemaMap,
759
766
  v._userFormats,
767
+ { noOrdinal: true },
760
768
  );
761
769
  if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
762
770
  // Hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) must travel with the
package/lib/draft7.js CHANGED
@@ -1,19 +1,36 @@
1
1
  'use strict'
2
2
 
3
+ // draft-06 is read through the same rules: it differs from draft-07 only in
4
+ // lacking `if`/`then`/`else` (and the content and readOnly annotations), so a
5
+ // draft-06 schema is a draft-07 schema with those three keywords inert. Before
6
+ // this, a draft-06 schema was read under 2020-12 and its tuple `items`,
7
+ // `additionalItems` and `dependencies` were not applied: 30 cases of the
8
+ // official draft-06 suite accepted documents they must reject.
3
9
  const DRAFT7_SCHEMAS = new Set([
4
10
  'http://json-schema.org/draft-07/schema#',
5
11
  'http://json-schema.org/draft-07/schema',
12
+ 'http://json-schema.org/draft-06/schema#',
13
+ 'http://json-schema.org/draft-06/schema',
14
+ ])
15
+ const DRAFT6_SCHEMAS = new Set([
16
+ 'http://json-schema.org/draft-06/schema#',
17
+ 'http://json-schema.org/draft-06/schema',
6
18
  ])
7
19
 
8
20
  function isDraft7(schema) {
9
- return !!(schema && schema.$schema && DRAFT7_SCHEMAS.has(schema.$schema))
21
+ return !!(schema && typeof schema.$schema === 'string' && DRAFT7_SCHEMAS.has(schema.$schema))
22
+ }
23
+
24
+ function isDraft6(schema) {
25
+ return !!(schema && typeof schema.$schema === 'string' && DRAFT6_SCHEMAS.has(schema.$schema))
10
26
  }
11
27
 
12
28
  // `force` applies the draft-07 rules to a document that declares no dialect
13
29
  // of its own, which is how a retrieved schema inherits the root's draft.
14
- function normalizeDraft7(schema, force) {
30
+ // `draft6` says the document, or the root it inherits from, is draft-06.
31
+ function normalizeDraft7(schema, force, draft6) {
15
32
  if (!force && !isDraft7(schema)) return schema
16
- _normalize(schema)
33
+ _normalize(schema, draft6 === true || isDraft6(schema))
17
34
  return schema
18
35
  }
19
36
 
@@ -21,9 +38,13 @@ function normalizeDraft7(schema, force) {
21
38
  // definitions are addressable by pointer, annotations are inert.
22
39
  const REF_SIBLINGS_KEPT = new Set(['$ref', '$defs', 'definitions', '$schema', '$comment', 'title', 'description', 'examples', 'default', 'readOnly', 'writeOnly'])
23
40
 
24
- function _normalize(schema) {
41
+ function _normalize(schema, d6) {
25
42
  if (typeof schema !== 'object' || schema === null) return
26
43
 
44
+ // draft-06 has no conditional: `if`, `then` and `else` are unknown keywords
45
+ // there, so they are dropped rather than applied.
46
+ if (d6) { delete schema.if; delete schema.then; delete schema.else }
47
+
27
48
  // In draft-07 an object with `$ref` is that reference and nothing else:
28
49
  // sibling keywords, `$id` included, are ignored. Dropping them here gives
29
50
  // every engine the same reading without each having to know the draft.
@@ -74,7 +95,7 @@ function _normalize(schema) {
74
95
  for (const key of objSubs) {
75
96
  if (schema[key] && typeof schema[key] === 'object') {
76
97
  for (const v of Object.values(schema[key])) {
77
- if (typeof v === 'object' && v !== null) _normalize(v)
98
+ if (typeof v === 'object' && v !== null) _normalize(v, d6)
78
99
  }
79
100
  }
80
101
  }
@@ -84,7 +105,7 @@ function _normalize(schema) {
84
105
  for (const key of arrSubs) {
85
106
  if (Array.isArray(schema[key])) {
86
107
  for (const s of schema[key]) {
87
- if (typeof s === 'object' && s !== null) _normalize(s)
108
+ if (typeof s === 'object' && s !== null) _normalize(s, d6)
88
109
  }
89
110
  }
90
111
  }
@@ -94,7 +115,7 @@ function _normalize(schema) {
94
115
  'additionalProperties', 'propertyNames']
95
116
  for (const key of singleSubs) {
96
117
  if (typeof schema[key] === 'object' && schema[key] !== null) {
97
- _normalize(schema[key])
118
+ _normalize(schema[key], d6)
98
119
  }
99
120
  }
100
121
  }
@@ -248,4 +269,81 @@ function _stripFormat(schema, seen) {
248
269
  }
249
270
  }
250
271
 
251
- module.exports = { isDraft7, normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions }
272
+ // Draft 2019-09 differs from 2020-12, which every engine here reads, in two
273
+ // places: the array form of `items` with `additionalItems` (2020-12 spells
274
+ // them `prefixItems` and `items`), and `$recursiveRef`/`$recursiveAnchor`,
275
+ // which 2020-12 generalised into `$dynamicRef`/`$dynamicAnchor`. Both are
276
+ // rewritten to the 2020-12 spelling. Before, a 2019-09 schema was read as
277
+ // 2020-12 as written, and 38 cases of the official 2019-09 suite accepted
278
+ // documents they must reject.
279
+ //
280
+ // The recursive pair maps one to one: a resource whose root says
281
+ // `$recursiveAnchor: true` gets a `$dynamicAnchor` of a reserved name, and
282
+ // `$recursiveRef: "#"` becomes a `$dynamicRef` to that name when its resource
283
+ // carries the anchor, and a plain `$ref: "#"` when it does not, which is what
284
+ // 2019-09 says it means then. The outward search for the outermost anchored
285
+ // resource is the same rule in both drafts.
286
+ const DRAFT2019_SCHEMAS = new Set([
287
+ 'https://json-schema.org/draft/2019-09/schema',
288
+ 'https://json-schema.org/draft/2019-09/schema#',
289
+ ])
290
+ const RECURSIVE_ANCHOR = '__ata_recursive_anchor'
291
+
292
+ function isDraft2019(schema) {
293
+ return !!(schema && typeof schema.$schema === 'string' && DRAFT2019_SCHEMAS.has(schema.$schema))
294
+ }
295
+
296
+ const SUB_OBJECT = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
297
+ const SUB_ARRAY = ['allOf', 'anyOf', 'oneOf', 'prefixItems', 'items']
298
+ const SUB_SINGLE = ['items', 'additionalItems', 'contains', 'not', 'if', 'then', 'else', 'additionalProperties',
299
+ 'propertyNames', 'unevaluatedItems', 'unevaluatedProperties', 'contentSchema']
300
+
301
+ function normalizeDraft2019(schema, force) {
302
+ if (!force && !isDraft2019(schema)) return schema
303
+ _normalize2019(schema, schema, false)
304
+ return schema
305
+ }
306
+
307
+ function _normalize2019(node, resourceRoot, isRoot) {
308
+ if (typeof node !== 'object' || node === null || Array.isArray(node)) return
309
+ // A subschema with its own `$id` starts a new resource.
310
+ const root = !isRoot && typeof node.$id === 'string' && !node.$id.startsWith('#') ? node : resourceRoot
311
+ if (node.$recursiveAnchor === true && node === root) node.$dynamicAnchor = RECURSIVE_ANCHOR
312
+ delete node.$recursiveAnchor
313
+ if (typeof node.$recursiveRef === 'string') {
314
+ if (node.$recursiveRef === '#' && root.$dynamicAnchor === RECURSIVE_ANCHOR) node.$dynamicRef = '#' + RECURSIVE_ANCHOR
315
+ else if (node.$ref === undefined) node.$ref = node.$recursiveRef
316
+ delete node.$recursiveRef
317
+ }
318
+ // Recurse before the items rewrite, so tuple members are walked as members.
319
+ for (const key of SUB_OBJECT) {
320
+ const sub = node[key]
321
+ if (sub && typeof sub === 'object') for (const v of Object.values(sub)) _normalize2019(v, root, false)
322
+ }
323
+ for (const key of SUB_ARRAY) {
324
+ if (Array.isArray(node[key])) for (const v of node[key]) _normalize2019(v, root, false)
325
+ }
326
+ for (const key of SUB_SINGLE) {
327
+ const sub = node[key]
328
+ if (sub && typeof sub === 'object' && !Array.isArray(sub)) _normalize2019(sub, root, false)
329
+ }
330
+ if (Array.isArray(node.items)) {
331
+ node.prefixItems = node.items
332
+ if (node.additionalItems !== undefined) { node.items = node.additionalItems; delete node.additionalItems } else delete node.items
333
+ } else {
334
+ delete node.additionalItems
335
+ }
336
+ }
337
+
338
+ // The draft a `$schema` value names, for the normalizers: 2 draft-06,
339
+ // 1 draft-07, 3 2019-09, 0 anything else. Takes the string so the caller
340
+ // reads the property once.
341
+ function draftCode(uri) {
342
+ if (typeof uri !== 'string') return 0
343
+ if (DRAFT6_SCHEMAS.has(uri)) return 2
344
+ if (DRAFT7_SCHEMAS.has(uri)) return 1
345
+ if (DRAFT2019_SCHEMAS.has(uri)) return 3
346
+ return 0
347
+ }
348
+
349
+ module.exports = { draftCode, isDraft7, isDraft6, isDraft2019, normalizeDraft2019, normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions }
@@ -77,7 +77,19 @@ function reprValue (v) {
77
77
  if (t === 'number' || t === 'boolean') return String(v);
78
78
  if (Array.isArray(v)) return `[array, ${v.length} items]`;
79
79
  if (t === 'object') {
80
- if (jsonSizeWithin(v, 60) >= 0) {
80
+ // The keys first: every key costs at least five characters serialised
81
+ // (`"k":` and a one-character value), so past twelve keys the 60-character
82
+ // body is out of reach and the size walk is skipped. The count is what the
83
+ // fallback prints, so it is taken once either way; the walk and the
84
+ // serialisation used to run on every container an error named, and a
85
+ // `required` or `unevaluatedProperties` error names its container.
86
+ let n;
87
+ try {
88
+ n = Object.keys(v).length;
89
+ } catch {
90
+ return '[object, unserializable]';
91
+ }
92
+ if (n <= 12 && jsonSizeWithin(v, 60) >= 0) {
81
93
  try {
82
94
  const s = JSON.stringify(v);
83
95
  if (s !== undefined && s.length <= 60) return s;
@@ -85,12 +97,6 @@ function reprValue (v) {
85
97
  return '[object, unserializable]';
86
98
  }
87
99
  }
88
- let n;
89
- try {
90
- n = Object.keys(v).length;
91
- } catch {
92
- return '[object, unserializable]';
93
- }
94
100
  return `[object, ${n} ${n === 1 ? 'key' : 'keys'}]`;
95
101
  }
96
102
  return `[${t}]`;
@@ -131,11 +137,12 @@ function resolveAt (err, data) {
131
137
  */
132
138
  // Observation-first wording. `message` stays as it is for parity, so this is a
133
139
  // separate field a consumer opts into.
134
- function detailFor (err, out) {
140
+ function detailFor (err, out, at) {
135
141
  const p = err.params || {};
136
142
  switch (err.keyword) {
137
143
  case 'type':
138
- return `expected ${p.type}, found ${typeNameOf(out.received)}`;
144
+ // From the value when the caller resolved it; from the repr otherwise.
145
+ return `expected ${p.type}, found ${(at === undefined || at === MISSING ? null : typeNameOfValue(at)) || typeNameOf(out.received)}`;
139
146
  case 'required':
140
147
  return `missing required property "${p.missingProperty}"`;
141
148
  case 'additionalProperties':
@@ -157,6 +164,24 @@ function detailFor (err, out) {
157
164
  }
158
165
  }
159
166
 
167
+ // The same names from the value itself, for the callers that hold it: the
168
+ // repr is a string built to be read, and parsing it back cost a regex per
169
+ // type error. Agrees with typeNameOf on every value reprValue can produce:
170
+ // a number that is not finite prints as NaN or Infinity, which the repr
171
+ // parser calls a value, and so does this.
172
+ // An object with its own toJSON prints as whatever that returns, so it is
173
+ // left to the repr route (null here).
174
+ function typeNameOfValue (v) {
175
+ if (v === null) return 'null';
176
+ switch (typeof v) {
177
+ case 'boolean': return 'boolean';
178
+ case 'string': return 'string';
179
+ case 'number': return Number.isFinite(v) ? 'number' : 'value';
180
+ case 'object': return Array.isArray(v) ? 'array' : typeof v.toJSON === 'function' ? null : 'object';
181
+ default: return 'value';
182
+ }
183
+ }
184
+
160
185
  // Derived from the repr in `received`, which is already a JSON-ish string.
161
186
  function typeNameOf (received) {
162
187
  if (received === undefined) return 'nothing';
@@ -252,7 +277,13 @@ function enrich (rawErr, opts) {
252
277
  // renderer can surface the closest variant's diagnostics. They are enriched
253
278
  // like any other error: passed through raw, they carried whatever the engine
254
279
  // that answered wrote, the generator's internal ordering key included.
255
- if (rawErr.branchErrors) out.branchErrors = rawErr.branchErrors.map((b) => enrich(b, data !== undefined ? { data } : null));
280
+ // A branch error the rich combined function already built is kept:
281
+ // enriching it again recomputed every field to the same values, and a
282
+ // collapsed oneOf paid that for each branch error on top of its own
283
+ // enrichment. Such an error carries `rank`, which only this enrichment and
284
+ // makeRich write; a standalone module's error carries a docUrl but no
285
+ // rank and is enriched as before (fromCompiled wraps those).
286
+ if (rawErr.branchErrors) out.branchErrors = rawErr.branchErrors.map((b) => (b !== null && typeof b === 'object' && b.rank !== undefined && b.docUrl !== undefined && b._o === undefined) ? b : enrich(b, data !== undefined ? { data } : null));
256
287
 
257
288
  // The frames come from the position map, which is a walk of the document.
258
289
  // Given lazily (validateJSON's rejections), the walk waits for the first
@@ -286,7 +317,7 @@ function enrich (rawErr, opts) {
286
317
  const sugg = suggestFor(out, data, at === MISSING ? undefined : at);
287
318
  if (sugg) out.suggestion = sugg;
288
319
 
289
- const detail = detailFor(rawErr, out);
320
+ const detail = detailFor(rawErr, out, at);
290
321
  if (detail !== undefined) out.detail = detail;
291
322
  out.rank = st.rank;
292
323
 
@@ -49,7 +49,7 @@ function makeRich (site, path, params, message, value, root, spPrefix) {
49
49
  };
50
50
  const sugg = suggestFor(out, root, at === MISSING ? undefined : at);
51
51
  if (sugg) out.suggestion = sugg;
52
- const detail = detailFor({ keyword: site.keyword, params }, out);
52
+ const detail = detailFor({ keyword: site.keyword, params }, out, at);
53
53
  if (detail !== undefined) out.detail = detail;
54
54
  out.rank = site.rank;
55
55
  return out;
@@ -9,7 +9,7 @@ const { compileFunction } = require('./compile-fn')
9
9
  // the first call that compiles errors. Everything here was written as part
10
10
  // of js-compiler.js and reads its helpers from that module's _internal.
11
11
  const core = require('./js-compiler')
12
- const { hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun } = core._internal
12
+ const { hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun } = core._internal
13
13
 
14
14
 
15
15
  function emitPtrEsc(ctx) {
@@ -289,7 +289,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
289
289
  if (hasUnresolvableRef(schema, eRootDefs, eAnchors, schemaMap, new Set())) return null
290
290
  if (needsBaseTracking(schema, schemaMap, new Set())) return null
291
291
 
292
- const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, shared: [], refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootE: schema, rootSchema: inputSchema, userFormats: userFormats || null,
292
+ const ctx = { noOrdinal: !!(sourceOpts && sourceOpts.noOrdinal), varCounter: 0, helperCode: [], rootDefs: eRootDefs, shared: [], refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootE: schema, rootSchema: inputSchema, userFormats: userFormats || null,
293
293
  // Custom format checkers referenced by the body, bound as
294
294
  // closure parameters below the way the other entry points do.
295
295
  closureVars: [], closureVals: [],
@@ -1143,7 +1143,8 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1143
1143
 
1144
1144
  // enum
1145
1145
  if (schema.enum) {
1146
- lines.push(`if(!(${enumCondition(ctx, schema.enum, v)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
1146
+ const ev = enumOperand(ctx, schema.enum, v, lines)
1147
+ lines.push(`if(!(${enumCondition(ctx, schema.enum, ev)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
1147
1148
  }
1148
1149
 
1149
1150
  // const — use canonical (sorted-key) comparison for objects
@@ -2330,7 +2331,8 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2330
2331
  const emitEnumConst = () => {
2331
2332
  // enum
2332
2333
  if (schema.enum) {
2333
- lines.push(`if(!(${enumCondition(ctx, schema.enum, v)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
2334
+ const ev = enumOperand(ctx, schema.enum, v, lines)
2335
+ lines.push(`if(!(${enumCondition(ctx, schema.enum, ev)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
2334
2336
  }
2335
2337
 
2336
2338
  // const
@@ -135,6 +135,21 @@ function emitConstant(ctx, value) {
135
135
  return name
136
136
  }
137
137
 
138
+ // The operand an `enum` test compares: `v` itself when it is a plain
139
+ // identifier, otherwise a local bound once. The test is a chain of `===`,
140
+ // one per member, and each used to repeat the access expression, so a
141
+ // 32-member enum on a required property read `_e3["tool"]` 32 times; on an
142
+ // array of items in several shapes those reads are polymorphic and cost
143
+ // 59 ns against 20 with the value in a local. The binding goes to `lines`
144
+ // ahead of the test.
145
+ const IDENT_RE = /^[A-Za-z_$][\w$]*$/
146
+ function enumOperand(ctx, vals, v, lines) {
147
+ if (IDENT_RE.test(v) || vals.length < 2) return v
148
+ const name = `_ev${ctx.varCounter++}`
149
+ lines.push(`const ${name}=${v}`)
150
+ return name
151
+ }
152
+
138
153
  // One `enum` test: primitives compare with `===`, which needs no call, and
139
154
  // object members go through structural equality.
140
155
  function enumCondition(ctx, vals, v) {
@@ -1316,6 +1331,14 @@ function sharedDefNames (root, defs) {
1316
1331
  }
1317
1332
  return out
1318
1333
  }
1334
+ // Raising this to inline small definitions at every site was measured and
1335
+ // rejected: SchemaStore's detekt applies one 200-character rule definition
1336
+ // at 293 sites, and inlining it took the verdict from 5287 ns to 452 but the
1337
+ // standalone module from 168 KB to 1692 KB and its compile to 33 ms. The
1338
+ // slowness of the shared function is megamorphic property reads (474 objects
1339
+ // of 72 shapes through one function); a for-in body for the shared function
1340
+ // was tried too and gave 1.5x, not enough to carry its complexity. Left as a
1341
+ // known trade: on that shape the shared function costs 27 ns a call.
1319
1342
  const SHARED_DEF_MIN_CHARS = 120
1320
1343
 
1321
1344
  function cyclicDefNames (defs) {
@@ -1986,8 +2009,32 @@ function sharedCodegenGate(schema, schemaMap) {
1986
2009
  _gateSchema = schema; _gateMap = schemaMap; _gateOk = ok
1987
2010
  return ok
1988
2011
  }
2012
+ // `unevaluatedProperties` or `unevaluatedItems` beside a `$ref` back to the
2013
+ // document root: the generated evaluated-set tracking does not credit what
2014
+ // the root evaluates through that reference, so a tree whose branches say
2015
+ // `{ unevaluatedProperties: false, $ref: '#' }` rejected valid branches (a
2016
+ // wrong reject, found through the 2019-09 `$recursiveRef` tests, and the same
2017
+ // in 2020-12). The interpreted engine answers it. A text check first: almost
2018
+ // no schema has both.
2019
+ function unevaluatedBesideRootRef(schema) {
2020
+ const text = JSON.stringify(schema)
2021
+ if (!text.includes('"$ref":"#"') || !/"unevaluated(?:Properties|Items)"/.test(text)) return false
2022
+ let found = false
2023
+ const seen = new Set()
2024
+ const walk = (n) => {
2025
+ if (found || n === null || typeof n !== 'object' || seen.has(n)) return
2026
+ seen.add(n)
2027
+ if (Array.isArray(n)) { for (const x of n) walk(x); return }
2028
+ if (n.$ref === '#' && (n.unevaluatedProperties !== undefined || n.unevaluatedItems !== undefined)) { found = true; return }
2029
+ for (const [k, v] of Object.entries(n)) if (k !== 'enum' && k !== 'const' && k !== 'default' && k !== 'examples') walk(v)
2030
+ }
2031
+ walk(schema)
2032
+ return found
2033
+ }
2034
+
1989
2035
  function sharedCodegenGateUncached(schema, schemaMap) {
1990
2036
  if (bothDefsContainers(schema)) return false
2037
+ if (unevaluatedBesideRootRef(schema)) return false
1991
2038
  if (hasSameInstanceRefCycle(schema)) return false
1992
2039
  if (!codegenSafe(schema, schemaMap)) return false
1993
2040
  if (needsBaseTracking(schema, schemaMap, new Set())) return false
@@ -3297,7 +3344,8 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3297
3344
 
3298
3345
  // enum
3299
3346
  if (schema.enum) {
3300
- lines.push(`if(!(${enumCondition(ctx, schema.enum, v)}))return false`)
3347
+ const ev = enumOperand(ctx, schema.enum, v, lines)
3348
+ lines.push(`if(!(${enumCondition(ctx, schema.enum, ev)}))return false`)
3301
3349
  }
3302
3350
 
3303
3351
  // const
@@ -5371,4 +5419,4 @@ Object.defineProperty(module.exports, '_expandDynamicScopes', { value: expandDyn
5371
5419
  // For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
5372
5420
  Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })
5373
5421
 
5374
- module.exports._internal = { hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun }
5422
+ module.exports._internal = { hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun }
@@ -2,8 +2,14 @@
2
2
 
3
3
  // The official meta-schemas, vendored so a `$ref` to one of them resolves
4
4
  // without a network. Draft 2020-12 is eight documents joined by $dynamicRef;
5
- // draft-07 is one. Sources: https://json-schema.org/draft/2020-12/schema and
6
- // http://json-schema.org/draft-07/schema, fetched 2026-08-23, unmodified.
5
+ // draft-07 and draft-06 are one each; 2019-09 is seven. Sources:
6
+ // https://json-schema.org/draft/2020-12/schema and
7
+ // http://json-schema.org/draft-07/schema, fetched 2026-08-23, and
8
+ // http://json-schema.org/draft-06/schema and
9
+ // https://json-schema.org/draft/2019-09/schema, fetched 2026-10-10, unmodified.
10
+ // The 2019-09 documents are read through the same 2019-09 rewrite as a
11
+ // caller's schema (lib/draft7.js) once, at load, since they use
12
+ // `$recursiveRef`, which the engines read as `$dynamicRef`.
7
13
  // Keyed by $id; index.js also registers the http/https and trailing-# spellings.
8
14
 
9
15
  const METASCHEMAS = new Map([
@@ -16,6 +22,19 @@ const METASCHEMAS = new Map([
16
22
  ["https://json-schema.org/draft/2020-12/meta/content", {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/content","$dynamicAnchor":"meta","title":"Content vocabulary meta-schema","type":["object","boolean"],"properties":{"contentEncoding":{"type":"string"},"contentMediaType":{"type":"string"},"contentSchema":{"$dynamicRef":"#meta"}}}],
17
23
  ["https://json-schema.org/draft/2020-12/meta/unevaluated", {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/unevaluated","$dynamicAnchor":"meta","title":"Unevaluated applicator vocabulary meta-schema","type":["object","boolean"],"properties":{"unevaluatedItems":{"$dynamicRef":"#meta"},"unevaluatedProperties":{"$dynamicRef":"#meta"}}}],
18
24
  ["http://json-schema.org/draft-07/schema#", {"$schema":"http://json-schema.org/draft-07/schema#","$id":"http://json-schema.org/draft-07/schema#","title":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"allOf":[{"$ref":"#/definitions/nonNegativeInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}},"type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference"},"$schema":{"type":"string","format":"uri"},"$ref":{"type":"string","format":"uri-reference"},"$comment":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"default":true,"readOnly":{"type":"boolean","default":false},"writeOnly":{"type":"boolean","default":false},"examples":{"type":"array","items":true},"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/definitions/nonNegativeInteger"},"minLength":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"$ref":"#"},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":true},"maxItems":{"$ref":"#/definitions/nonNegativeInteger"},"minItems":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"contains":{"$ref":"#"},"maxProperties":{"$ref":"#/definitions/nonNegativeInteger"},"minProperties":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"$ref":"#"},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"propertyNames":{"format":"regex"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"propertyNames":{"$ref":"#"},"const":true,"enum":{"type":"array","items":true,"minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"if":{"$ref":"#"},"then":{"$ref":"#"},"else":{"$ref":"#"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"default":true}],
25
+ ["http://json-schema.org/draft-06/schema#", {"$schema":"http://json-schema.org/draft-06/schema#","$id":"http://json-schema.org/draft-06/schema#","title":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"allOf":[{"$ref":"#/definitions/nonNegativeInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}},"type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference"},"$schema":{"type":"string","format":"uri"},"$ref":{"type":"string","format":"uri-reference"},"title":{"type":"string"},"description":{"type":"string"},"default":{},"examples":{"type":"array","items":{}},"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/definitions/nonNegativeInteger"},"minLength":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"$ref":"#"},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":{}},"maxItems":{"$ref":"#/definitions/nonNegativeInteger"},"minItems":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"contains":{"$ref":"#"},"maxProperties":{"$ref":"#/definitions/nonNegativeInteger"},"minProperties":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"$ref":"#"},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"propertyNames":{"format":"regex"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"propertyNames":{"$ref":"#"},"const":{},"enum":{"type":"array","minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"default":{}}],
26
+ ["https://json-schema.org/draft/2019-09/schema", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/schema","$vocabulary":{"https://json-schema.org/draft/2019-09/vocab/core":true,"https://json-schema.org/draft/2019-09/vocab/applicator":true,"https://json-schema.org/draft/2019-09/vocab/validation":true,"https://json-schema.org/draft/2019-09/vocab/meta-data":true,"https://json-schema.org/draft/2019-09/vocab/format":false,"https://json-schema.org/draft/2019-09/vocab/content":true},"$recursiveAnchor":true,"title":"Core and Validation specifications meta-schema","allOf":[{"$ref":"meta/core"},{"$ref":"meta/applicator"},{"$ref":"meta/validation"},{"$ref":"meta/meta-data"},{"$ref":"meta/format"},{"$ref":"meta/content"}],"type":["object","boolean"],"properties":{"definitions":{"$comment":"While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.","type":"object","additionalProperties":{"$recursiveRef":"#"},"default":{}},"dependencies":{"$comment":"\"dependencies\" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to \"dependentSchemas\" and \"dependentRequired\"","type":"object","additionalProperties":{"anyOf":[{"$recursiveRef":"#"},{"$ref":"meta/validation#/$defs/stringArray"}]}}}}],
27
+ ["https://json-schema.org/draft/2019-09/meta/core", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/core","$recursiveAnchor":true,"title":"Core vocabulary meta-schema","type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference","$comment":"Non-empty fragments not allowed.","pattern":"^[^#]*#?$"},"$schema":{"type":"string","format":"uri"},"$anchor":{"type":"string","pattern":"^[A-Za-z][-A-Za-z0-9.:_]*$"},"$ref":{"type":"string","format":"uri-reference"},"$recursiveRef":{"type":"string","format":"uri-reference"},"$recursiveAnchor":{"type":"boolean","default":false},"$vocabulary":{"type":"object","propertyNames":{"type":"string","format":"uri"},"additionalProperties":{"type":"boolean"}},"$comment":{"type":"string"},"$defs":{"type":"object","additionalProperties":{"$recursiveRef":"#"},"default":{}}}}],
28
+ ["https://json-schema.org/draft/2019-09/meta/applicator", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/applicator","$recursiveAnchor":true,"title":"Applicator vocabulary meta-schema","type":["object","boolean"],"properties":{"additionalItems":{"$recursiveRef":"#"},"unevaluatedItems":{"$recursiveRef":"#"},"items":{"anyOf":[{"$recursiveRef":"#"},{"$ref":"#/$defs/schemaArray"}]},"contains":{"$recursiveRef":"#"},"additionalProperties":{"$recursiveRef":"#"},"unevaluatedProperties":{"$recursiveRef":"#"},"properties":{"type":"object","additionalProperties":{"$recursiveRef":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$recursiveRef":"#"},"propertyNames":{"format":"regex"},"default":{}},"dependentSchemas":{"type":"object","additionalProperties":{"$recursiveRef":"#"}},"propertyNames":{"$recursiveRef":"#"},"if":{"$recursiveRef":"#"},"then":{"$recursiveRef":"#"},"else":{"$recursiveRef":"#"},"allOf":{"$ref":"#/$defs/schemaArray"},"anyOf":{"$ref":"#/$defs/schemaArray"},"oneOf":{"$ref":"#/$defs/schemaArray"},"not":{"$recursiveRef":"#"}},"$defs":{"schemaArray":{"type":"array","minItems":1,"items":{"$recursiveRef":"#"}}}}],
29
+ ["https://json-schema.org/draft/2019-09/meta/validation", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/validation","$recursiveAnchor":true,"title":"Validation vocabulary meta-schema","type":["object","boolean"],"properties":{"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/$defs/nonNegativeInteger"},"minLength":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"maxItems":{"$ref":"#/$defs/nonNegativeInteger"},"minItems":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"maxContains":{"$ref":"#/$defs/nonNegativeInteger"},"minContains":{"$ref":"#/$defs/nonNegativeInteger","default":1},"maxProperties":{"$ref":"#/$defs/nonNegativeInteger"},"minProperties":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"required":{"$ref":"#/$defs/stringArray"},"dependentRequired":{"type":"object","additionalProperties":{"$ref":"#/$defs/stringArray"}},"const":true,"enum":{"type":"array","items":true},"type":{"anyOf":[{"$ref":"#/$defs/simpleTypes"},{"type":"array","items":{"$ref":"#/$defs/simpleTypes"},"minItems":1,"uniqueItems":true}]}},"$defs":{"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"$ref":"#/$defs/nonNegativeInteger","default":0},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}}}],
30
+ ["https://json-schema.org/draft/2019-09/meta/meta-data", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/meta-data","$recursiveAnchor":true,"title":"Meta-data vocabulary meta-schema","type":["object","boolean"],"properties":{"title":{"type":"string"},"description":{"type":"string"},"default":true,"deprecated":{"type":"boolean","default":false},"readOnly":{"type":"boolean","default":false},"writeOnly":{"type":"boolean","default":false},"examples":{"type":"array","items":true}}}],
31
+ ["https://json-schema.org/draft/2019-09/meta/format", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/format","$recursiveAnchor":true,"title":"Format vocabulary meta-schema","type":["object","boolean"],"properties":{"format":{"type":"string"}}}],
32
+ ["https://json-schema.org/draft/2019-09/meta/content", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/content","$recursiveAnchor":true,"title":"Content vocabulary meta-schema","type":["object","boolean"],"properties":{"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"contentSchema":{"$recursiveRef":"#"}}}],
19
33
  ]);
20
34
 
35
+ {
36
+ const { isDraft2019, normalizeDraft2019 } = require('./draft7')
37
+ for (const [id, doc] of METASCHEMAS) if (isDraft2019(doc)) METASCHEMAS.set(id, normalizeDraft2019(JSON.parse(JSON.stringify(doc)), true))
38
+ }
39
+
21
40
  module.exports = { METASCHEMAS };
@@ -3,34 +3,56 @@
3
3
  const { parse } = require('./safe-regex')
4
4
 
5
5
  // True when the platform RegExp, a backtracking engine, also runs `src` in
6
- // linear time, so it can take the pattern: every unbounded quantifier on one
7
- // character, class or `.`; at most one of them, and then anchored with `^`;
8
- // a finite repeat may cover a group that holds no unbounded quantifier; few
9
- // bounded choices in all; only escapes both engines read alike; nothing
10
- // outside the BMP, so both see the same UTF-16 units.
11
- // tests/test_regex_native_linear.js holds it.
6
+ // linear time, so it can take the pattern instead of the linear-time engine
7
+ // in safe-regex.js, which costs three to five times as much per character.
8
+ // Two rules, and a pattern passes on either:
9
+ //
10
+ // The simple rule, the original one: every unbounded quantifier is on one
11
+ // character, class or `.`; at most one of them, and then the pattern is
12
+ // anchored with `^` on every branch; a finite repeat may cover a group that
13
+ // holds no unbounded quantifier; few bounded choices in all. One loop whose
14
+ // body cannot be split two ways, exited at most once per position, with
15
+ // bounded work after it.
16
+ //
17
+ // The deterministic rule, for the patterns the simple one declines: dotted
18
+ // identifiers, `[^/]+/[^/]+`, version numbers. On the Glushkov construction
19
+ // the characters, classes and dots are positions. Around every unbounded
20
+ // loop the next character must decide the single way to continue: the
21
+ // positions that can follow a position inside a loop accept pairwise
22
+ // disjoint characters, and so do the positions a match can start at when a
23
+ // loop is among them; a position reaches a follower by one route only, since
24
+ // `(a+)+` reaches `a` from `a` through the inner loop and again through the
25
+ // outer one and that double route is what a backtracking engine explores
26
+ // exponentially; a loop body must not match the empty string, since `(a*)*`
27
+ // iterates emptily without bound. Ambiguity away from any loop, `(pwa|PWA)`,
28
+ // costs a bounded factor and is allowed, as the simple rule allows it. A
29
+ // bounded repeat is a loop only when its body holds an unbounded one, since
30
+ // `(\w+\s?){5}` splits five ways where `(?:[A-F0-9]{2}:){31}` counts. With
31
+ // an unbounded quantifier the pattern must be anchored with `^` on every
32
+ // branch, since an unanchored search retries from each position.
33
+ //
34
+ // Both rules allow only escapes both engines read alike and nothing outside
35
+ // the BMP, so both see the same UTF-16 units. tests/test_regex_native_linear.js
36
+ // holds them: every accepted pattern answers as the linear engine does and
37
+ // stays fast on inputs built to make a backtracking engine retry.
12
38
  const NATIVE_ESCAPES = new Set(['d', 'D', 'w', 'W', 's', 'S', 'n', 'r', 't', 'f', 'v', '.', '\\', '/', '^', '$', '|', '?', '*', '+', '(', ')', '[', ']', '{', '}', '-', 'x', 'u'])
13
- function nativeIsLinear (src) {
14
- if (typeof src !== 'string' || src.length > 512) return false
15
- for (let i = 0; i < src.length; i++) {
16
- const c = src.charCodeAt(i)
17
- if (c >= 0xd800 && c <= 0xdfff) return false
18
- if (src[i] === '\\') {
19
- const e = src[i + 1]
20
- if (!NATIVE_ESCAPES.has(e)) return false
21
- if (e === 'x' && !/^[0-9a-fA-F]{2}$/.test(src.slice(i + 2, i + 4))) return false
22
- if (e === 'u' && !/^[0-9a-fA-F]{4}$/.test(src.slice(i + 2, i + 6))) return false
23
- i++
24
- }
25
- }
26
- let ast
27
- try { ast = parse(src) } catch { return false }
39
+ const MAX_BOUNDED_CHOICES = 64
40
+
41
+ const atom = (n) => n.t === 'char' || n.t === 'class' || n.t === 'any'
42
+
43
+ // Anchored at the start on every top-level branch.
44
+ function anchored (ast) {
45
+ let top = ast
46
+ while (top.t === 'group') top = top.child
47
+ const branches = top.t === 'alt' ? top.opts : [top]
48
+ const first = (n) => { while (n && n.t === 'group') n = n.child; return n && n.t === 'concat' ? first(n.parts[0]) : n }
49
+ return branches.every((b) => { const f = first(b); return f && f.t === 'bol' })
50
+ }
51
+
52
+ function simpleRule (ast) {
28
53
  // Per subtree: how many unbounded quantifiers it holds (`u`) and how many
29
54
  // bounded choices a backtracking engine could try (`b`), or null where the
30
- // shape is not provably linear. A finite repeat of a group is linear when
31
- // the group holds no unbounded quantifier: `(?:[A-F0-9]{2}:){31}` has
32
- // nothing to retry, and its choices multiply through the nesting.
33
- const atom = (n) => n.t === 'char' || n.t === 'class' || n.t === 'any'
55
+ // shape is not provably linear.
34
56
  const info = (n) => {
35
57
  switch (n.t) {
36
58
  case 'star': case 'plus':
@@ -56,19 +78,154 @@ function nativeIsLinear (src) {
56
78
  }
57
79
  }
58
80
  const r = info(ast)
59
- const ok = r !== null
60
- const unbounded = ok ? r.u : 0
61
- const bounded = ok ? r.b : 0
62
- if (!ok || unbounded > 1 || bounded > 64) return false
63
- if (unbounded === 1) {
64
- // Anchored at the start on every top-level branch.
65
- let top = ast
66
- while (top.t === 'group') top = top.child
67
- const branches = top.t === 'alt' ? top.opts : [top]
68
- const first = (n) => { while (n && n.t === 'group') n = n.child; return n && n.t === 'concat' ? first(n.parts[0]) : n }
69
- if (!branches.every((b) => { const f = first(b); return f && f.t === 'bol' })) return false
81
+ if (r === null || r.u > 1 || r.b > MAX_BOUNDED_CHOICES) return false
82
+ return r.u === 0 || anchored(ast)
83
+ }
84
+
85
+ // The characters a position accepts: 128 ASCII bits and one bit for "some
86
+ // code point at 128 or above", which keeps two classes with non-ASCII
87
+ // members apart only when one of them has none. Conservative in the right
88
+ // direction: an overlap that is not there only sends a pattern to the
89
+ // linear engine.
90
+ function charSet (node) {
91
+ const bits = new Uint8Array(128)
92
+ let high = 0
93
+ if (node.t === 'any') {
94
+ bits.fill(1); bits[10] = 0; bits[13] = 0; high = 1
95
+ } else if (node.t === 'char') {
96
+ if (node.c < 128) bits[node.c] = 1; else high = 1
97
+ } else {
98
+ for (const [lo, hi] of node.ranges) {
99
+ for (let c = Math.max(0, lo), top = Math.min(127, hi); c <= top; c++) bits[c] = 1
100
+ if (hi >= 128) high = 1
101
+ }
102
+ if (node.neg) { for (let c = 0; c < 128; c++) bits[c] ^= 1; high = 1 }
70
103
  }
71
- return true
104
+ return { bits, high }
105
+ }
106
+
107
+ function overlap (a, b) {
108
+ if (a.high && b.high) return true
109
+ for (let c = 0; c < 128; c++) if (a.bits[c] & b.bits[c]) return true
110
+ return false
111
+ }
112
+
113
+ // Whether a subtree holds an unbounded quantifier.
114
+ function hasUnbounded (n) {
115
+ switch (n.t) {
116
+ case 'star': case 'plus': return true
117
+ case 'repeat': return n.max === Infinity || hasUnbounded(n.child)
118
+ case 'quest': case 'group': return hasUnbounded(n.child)
119
+ case 'concat': return n.parts.some(hasUnbounded)
120
+ case 'alt': return n.opts.some(hasUnbounded)
121
+ default: return false
122
+ }
123
+ }
124
+
125
+ function deterministicRule (ast) {
126
+ const sets = []
127
+ const follow = []
128
+ const inLoop = []
129
+ let ok = true
130
+ let unbounded = 0
131
+ let bounded = 0
132
+ let depth = 0
133
+ // Pairwise disjoint, required when a loop position is among them.
134
+ const disjoint = (ids) => {
135
+ const arr = Array.from(ids)
136
+ if (!arr.some((p) => inLoop[p])) return
137
+ for (let i = 0; i < arr.length && ok; i++) {
138
+ for (let j = i + 1; j < arr.length; j++) if (overlap(sets[arr[i]], sets[arr[j]])) { ok = false; return }
139
+ }
140
+ }
141
+ const link = (from, to) => {
142
+ for (const p of to) {
143
+ if (follow[from].has(p)) { ok = false; return }
144
+ follow[from].add(p)
145
+ }
146
+ }
147
+ const empty = () => ({ nullable: true, first: new Set(), last: new Set() })
148
+ const walk = (n) => {
149
+ if (!ok) return empty()
150
+ switch (n.t) {
151
+ case 'char': case 'class': case 'any': {
152
+ const id = sets.length
153
+ sets.push(charSet(n))
154
+ follow.push(new Set())
155
+ inLoop.push(depth > 0)
156
+ return { nullable: false, first: new Set([id]), last: new Set([id]) }
157
+ }
158
+ case 'bol': case 'eol': return empty()
159
+ case 'group': return walk(n.child)
160
+ case 'star': case 'plus': case 'quest': case 'repeat': {
161
+ const infinite = n.t === 'star' || n.t === 'plus' || (n.t === 'repeat' && n.max === Infinity)
162
+ const loops = infinite || (n.t === 'repeat' && n.max > 1 && hasUnbounded(n.child))
163
+ const optional = n.t === 'star' || n.t === 'quest' || (n.t === 'repeat' && n.min === 0)
164
+ if (loops) depth++
165
+ const c = walk(n.child)
166
+ if (loops) depth--
167
+ if (infinite) {
168
+ unbounded++
169
+ if (c.nullable) ok = false
170
+ } else if (n.t === 'quest') {
171
+ bounded += 1
172
+ } else {
173
+ bounded += (n.max - n.min)
174
+ if (!atom(n.child)) bounded += n.max
175
+ }
176
+ if (loops) for (const l of c.last) link(l, c.first)
177
+ return { nullable: c.nullable || optional, first: c.first, last: c.last }
178
+ }
179
+ case 'concat': {
180
+ let acc = empty()
181
+ for (const part of n.parts) {
182
+ const r = walk(part)
183
+ for (const l of acc.last) link(l, r.first)
184
+ const first = new Set(acc.first)
185
+ if (acc.nullable) for (const f of r.first) first.add(f)
186
+ const last = new Set(r.last)
187
+ if (r.nullable) for (const l of acc.last) last.add(l)
188
+ acc = { nullable: acc.nullable && r.nullable, first, last }
189
+ }
190
+ return acc
191
+ }
192
+ case 'alt': {
193
+ const rs = n.opts.map(walk)
194
+ const first = new Set()
195
+ const last = new Set()
196
+ for (const r of rs) { for (const f of r.first) first.add(f); for (const l of r.last) last.add(l) }
197
+ return { nullable: rs.some((r) => r.nullable), first, last }
198
+ }
199
+ default:
200
+ ok = false
201
+ return empty()
202
+ }
203
+ }
204
+ const root = walk(ast)
205
+ if (!ok) return false
206
+ disjoint(root.first)
207
+ for (let p = 0; p < sets.length && ok; p++) disjoint(follow[p])
208
+ if (!ok) return false
209
+ if (bounded > MAX_BOUNDED_CHOICES) return false
210
+ return unbounded === 0 || anchored(ast)
211
+ }
212
+
213
+ function nativeIsLinear (src) {
214
+ if (typeof src !== 'string' || src.length > 512) return false
215
+ for (let i = 0; i < src.length; i++) {
216
+ const c = src.charCodeAt(i)
217
+ if (c >= 0xd800 && c <= 0xdfff) return false
218
+ if (src[i] === '\\') {
219
+ const e = src[i + 1]
220
+ if (!NATIVE_ESCAPES.has(e)) return false
221
+ if (e === 'x' && !/^[0-9a-fA-F]{2}$/.test(src.slice(i + 2, i + 4))) return false
222
+ if (e === 'u' && !/^[0-9a-fA-F]{4}$/.test(src.slice(i + 2, i + 6))) return false
223
+ i++
224
+ }
225
+ }
226
+ let ast
227
+ try { ast = parse(src) } catch { return false }
228
+ return simpleRule(ast) || deterministicRule(ast)
72
229
  }
73
230
 
74
231
  module.exports = { nativeIsLinear }
package/lib/safe-regex.js CHANGED
@@ -410,7 +410,8 @@ function __ataSafeRegex () {
410
410
  id = states.length
411
411
  let isMatch = false
412
412
  for (let k = 0; k < pcs.length; k++) if (ops[pcs[k]] === OP_MATCH) { isMatch = true; break }
413
- states.push({ pcs: Int32Array.from(pcs), isMatch, next: new Int32Array(128).fill(-2), nextEnd: new Int32Array(128).fill(-2) })
413
+ // `dead`: an anchored match with no thread left cannot recover.
414
+ states.push({ pcs: Int32Array.from(pcs), isMatch, dead: anchored && pcs.length === 0, next: new Int32Array(128).fill(-2), nextEnd: new Int32Array(128).fill(-2) })
414
415
  stateIds.set(key, id)
415
416
  return id
416
417
  }
@@ -452,23 +453,36 @@ function __ataSafeRegex () {
452
453
  }
453
454
  if (id < 0) return testNFA(s)
454
455
  let state = states[id]
455
- for (let pos = 0; pos < len; pos++) {
456
+ if (len === 0) return state.isMatch
457
+ // The last character is stepped with the end-of-input flag set and is
458
+ // handled after the loop, so the loop body selects no table and tests
459
+ // no position.
460
+ const last = len - 1
461
+ for (let pos = 0; pos < last; pos++) {
456
462
  if (state.isMatch) return true
457
463
  const c = s.charCodeAt(pos)
458
- const atEnd = pos + 1 === len
459
464
  let nid
460
465
  if (c < 128) {
461
- const table = atEnd ? state.nextEnd : state.next
462
- nid = table[c]
463
- if (nid === -2) { nid = step(state, c, atEnd); table[c] = nid }
466
+ nid = state.next[c]
467
+ if (nid === -2) { nid = step(state, c, false); state.next[c] = nid }
464
468
  } else {
465
- nid = step(state, c, atEnd)
469
+ nid = step(state, c, false)
466
470
  }
467
471
  if (nid < 0) return testNFA(s)
468
472
  state = states[nid]
469
- if (anchored && state.pcs.length === 0) return false
473
+ if (state.dead) return false
470
474
  }
471
- return state.isMatch
475
+ if (state.isMatch) return true
476
+ const c = s.charCodeAt(last)
477
+ let nid
478
+ if (c < 128) {
479
+ nid = state.nextEnd[c]
480
+ if (nid === -2) { nid = step(state, c, true); state.nextEnd[c] = nid }
481
+ } else {
482
+ nid = step(state, c, true)
483
+ }
484
+ if (nid < 0) return testNFA(s)
485
+ return states[nid].isMatch
472
486
  }
473
487
 
474
488
  return function test (s) {
@@ -25,7 +25,7 @@ function _registerCodegen(engine) {
25
25
  _codegen = engine;
26
26
  ({ compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined } = engine.jsCompiler);
27
27
  }
28
- const { normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions } = require("./draft7");
28
+ const { draftCode, normalizeDraft2019, normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions } = require("./draft7");
29
29
  const { enabledKeywords, stripDisabledKeywords } = require("./vocabularies");
30
30
  const { needsNormalization } = require("./schema-scan");
31
31
  const { isV1Dialect } = require("./dialect");
@@ -426,21 +426,26 @@ 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
+ // `inheritDraft7` is the root's draft for a document that declares none of
430
+ // its own: 0 or absent for none, 1 for draft-07, 2 for draft-06, 3 for 2019-09.
429
431
  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
432
+ const declared = s && typeof s === 'object' ? s.$schema : undefined
433
+ const code = declared !== undefined ? draftCode(declared) : (inheritDraft7 || 0)
434
+ const needsDraft7 = code === 1 || code === 2
435
+ const draft6 = code === 2
436
+ const draft2019 = code === 3
434
437
  // One walk answers whether there is anything to do. Almost always there is
435
438
  // not, and then the serialize, clone, normalize, serialize, compare below is
436
439
  // work spent to find that out. The walk over-reports rather than under, so a
437
440
  // schema it clears is one no normalizer would have touched;
438
441
  // `tests/test_schema_scan.js` holds that direction against the whole suite.
439
- if (!needsNormalization(s, needsDraft7)) return s
442
+ // The scan does not look for `if`/`then`/`else`, which draft-06 drops.
443
+ if (!draft6 && !draft2019 && !needsNormalization(s, needsDraft7)) return s
440
444
 
441
445
  const str = JSON.stringify(s)
442
446
  const copy = _deepCloneWithSymbols(s)
443
- if (needsDraft7) normalizeDraft7(copy, true)
447
+ if (needsDraft7) normalizeDraft7(copy, true, draft6)
448
+ if (draft2019) normalizeDraft2019(copy, true)
444
449
  normalizeNullable(copy)
445
450
  normalizeExclusiveBounds(copy)
446
451
  // Return original when normalization produced no change, copy otherwise.
@@ -467,7 +472,7 @@ function declaredId(original, normalized) {
467
472
  return undefined
468
473
  }
469
474
 
470
- // `inheritDraft7` is true when the root schema is draft-07: a retrieved
475
+ // `inheritDraft7` is non-zero when the root schema is draft-07 or draft-06: a retrieved
471
476
  // document that declares no dialect is read under the root's draft.
472
477
  // The map is derived entirely from what the caller passed, so the same
473
478
  // `schemas` gives the same map. A server building one validator per route over
@@ -485,12 +490,12 @@ function buildSchemaMap(schemas, inheritDraft7) {
485
490
  if (!schemas) return null
486
491
  const byDraft = _schemaMapCache.get(schemas)
487
492
  if (byDraft) {
488
- const hit = byDraft[inheritDraft7 ? 1 : 0]
493
+ const hit = byDraft[inheritDraft7 || 0]
489
494
  if (hit) return hit
490
495
  }
491
496
  const map = _buildSchemaMap(schemas, inheritDraft7)
492
- const slot = byDraft || [null, null]
493
- slot[inheritDraft7 ? 1 : 0] = map
497
+ const slot = byDraft || [null, null, null, null]
498
+ slot[inheritDraft7 || 0] = map
494
499
  if (!byDraft) _schemaMapCache.set(schemas, slot)
495
500
  return map
496
501
  }
@@ -676,8 +681,7 @@ class Validator {
676
681
  // uses, and a benchmark timing construction measures construction.
677
682
  // When schema is a string, JSON.parse already produces a fresh object.
678
683
  const raw = typeof schema === "string" ? JSON.parse(schema) : schema;
679
- const rootIsDraft7 = !!(raw && typeof raw === 'object' && typeof raw.$schema === 'string' &&
680
- (raw.$schema === 'http://json-schema.org/draft-07/schema#' || raw.$schema === 'http://json-schema.org/draft-07/schema'));
684
+ const rootIsDraft7 = raw && typeof raw === 'object' ? draftCode(raw.$schema) : 0;
681
685
 
682
686
  // Built here rather than below because `$vocabulary` is resolved against
683
687
  // it, and that resolution waits until compilation so a meta-schema
@@ -1561,8 +1565,7 @@ class Validator {
1561
1565
  // Normalize a copy so the caller's object is never mutated. A document
1562
1566
  // without a dialect of its own is read under the root's draft.
1563
1567
  const root = this._schemaObj
1564
- const rootIsDraft7 = !!(root && typeof root === 'object' && typeof root.$schema === 'string' &&
1565
- (root.$schema === 'http://json-schema.org/draft-07/schema#' || root.$schema === 'http://json-schema.org/draft-07/schema'))
1568
+ const rootIsDraft7 = root && typeof root === 'object' ? draftCode(root.$schema) : 0
1566
1569
  const normalized = _normalizeCallerSchema(schema, rootIsDraft7)
1567
1570
  this._ownSchemaMap()
1568
1571
  this._schemaMap.set(normalized.$id, normalized)
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.48.0';
10
+ module.exports = '1.49.0';
@@ -50,6 +50,18 @@ function keywordsByVocabulary() {
50
50
  new Set(Object.keys(meta.properties)),
51
51
  )
52
52
  }
53
+ // Draft 2019-09's vocabularies, from its own vendored meta-schemas. A
54
+ // 2019-09 dialect names these URIs in `$vocabulary`.
55
+ for (const name of ['core', 'applicator', 'validation', 'meta-data', 'format', 'content']) {
56
+ const meta = require('./metaschemas').METASCHEMAS.get(
57
+ `https://json-schema.org/draft/2019-09/meta/${name}`,
58
+ )
59
+ if (!meta || !meta.properties) continue
60
+ byVocabulary.set(
61
+ `https://json-schema.org/draft/2019-09/vocab/${name}`,
62
+ new Set(Object.keys(meta.properties)),
63
+ )
64
+ }
53
65
  // The one vocabulary with no document to read. `format-assertion` is an
54
66
  // alternative to `format-annotation` rather than part of the standard
55
67
  // dialect, so its meta-schema is not among the vendored ones. It defines
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.48.0",
3
+ "version": "1.49.0",
4
4
  "description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
5
5
  "type": "commonjs",
6
6
  "main": "index.js",
@@ -72,7 +72,7 @@
72
72
  "rebuild": "cmake-js rebuild --target ata",
73
73
  "test": "node tests/run.js",
74
74
  "bench:size": "node benchmark/bench_aot_size.mjs",
75
- "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
75
+ "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1 && node tests/run_suite.js draft6 && node tests/run_suite.js draft2019-09",
76
76
  "test:compat": "node tests/test_compat.js",
77
77
  "test:standard-schema": "node tests/test_standard_schema.js",
78
78
  "test:browser": "node tests/test_browser.js",
@@ -152,13 +152,13 @@
152
152
  "LICENSE"
153
153
  ],
154
154
  "optionalDependencies": {
155
- "@ata-validator/native-darwin-arm64": "1.48.0",
156
- "@ata-validator/native-darwin-x64": "1.48.0",
157
- "@ata-validator/native-linux-arm64-gnu": "1.48.0",
158
- "@ata-validator/native-linux-arm64-musl": "1.48.0",
159
- "@ata-validator/native-linux-x64-gnu": "1.48.0",
160
- "@ata-validator/native-linux-x64-musl": "1.48.0",
161
- "@ata-validator/native-win32-x64": "1.48.0"
155
+ "@ata-validator/native-darwin-arm64": "1.49.0",
156
+ "@ata-validator/native-darwin-x64": "1.49.0",
157
+ "@ata-validator/native-linux-arm64-gnu": "1.49.0",
158
+ "@ata-validator/native-linux-arm64-musl": "1.49.0",
159
+ "@ata-validator/native-linux-x64-gnu": "1.49.0",
160
+ "@ata-validator/native-linux-x64-musl": "1.49.0",
161
+ "@ata-validator/native-win32-x64": "1.49.0"
162
162
  },
163
163
  "peerDependencies": {
164
164
  "yaml": "^2.0.0"