z-schema 10.0.0 → 12.0.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.
Files changed (138) hide show
  1. package/README.md +35 -17
  2. package/cjs/index.d.ts +345 -34
  3. package/cjs/index.js +4446 -1685
  4. package/dist/errors.js +5 -0
  5. package/dist/format-validators.js +131 -107
  6. package/dist/json-schema-versions.js +4 -1
  7. package/dist/json-schema.js +50 -16
  8. package/dist/json-validation.js +524 -669
  9. package/dist/report.js +37 -16
  10. package/dist/schema-cache.js +76 -18
  11. package/dist/schema-compiler.js +72 -47
  12. package/dist/schema-validator.js +117 -52
  13. package/dist/schemas/draft-07-schema.json +172 -0
  14. package/dist/schemas/draft-2019-09-meta-applicator.json +52 -0
  15. package/dist/schemas/draft-2019-09-meta-content.json +12 -0
  16. package/dist/schemas/draft-2019-09-meta-core.json +53 -0
  17. package/dist/schemas/draft-2019-09-meta-format.json +10 -0
  18. package/dist/schemas/draft-2019-09-meta-meta-data.json +32 -0
  19. package/dist/schemas/draft-2019-09-meta-validation.json +94 -0
  20. package/dist/schemas/draft-2019-09-schema.json +41 -0
  21. package/dist/schemas/draft-2020-12-meta-applicator.json +44 -0
  22. package/dist/schemas/draft-2020-12-meta-content.json +12 -0
  23. package/dist/schemas/draft-2020-12-meta-core.json +47 -0
  24. package/dist/schemas/draft-2020-12-meta-format-annotation.json +10 -0
  25. package/dist/schemas/draft-2020-12-meta-format-assertion.json +10 -0
  26. package/dist/schemas/draft-2020-12-meta-meta-data.json +32 -0
  27. package/dist/schemas/draft-2020-12-meta-unevaluated.json +11 -0
  28. package/dist/schemas/draft-2020-12-meta-validation.json +94 -0
  29. package/dist/schemas/draft-2020-12-schema.json +57 -0
  30. package/dist/types/errors.d.ts +4 -0
  31. package/dist/types/index.d.ts +2 -1
  32. package/dist/types/json-schema-versions.d.ts +128 -9
  33. package/dist/types/json-schema.d.ts +28 -11
  34. package/dist/types/json-validation.d.ts +2 -3
  35. package/dist/types/report.d.ts +14 -4
  36. package/dist/types/schema-cache.d.ts +7 -0
  37. package/dist/types/schema-compiler.d.ts +5 -3
  38. package/dist/types/schema-validator.d.ts +2 -2
  39. package/dist/types/utils/array.d.ts +8 -1
  40. package/dist/types/utils/base64.d.ts +2 -0
  41. package/dist/types/utils/clone.d.ts +1 -1
  42. package/dist/types/utils/date.d.ts +1 -0
  43. package/dist/types/utils/hostname.d.ts +2 -0
  44. package/dist/types/utils/json.d.ts +2 -1
  45. package/dist/types/utils/properties.d.ts +0 -1
  46. package/dist/types/utils/time.d.ts +12 -0
  47. package/dist/types/utils/unicode.d.ts +3 -12
  48. package/dist/types/validation/array.d.ts +12 -0
  49. package/dist/types/validation/combinators.d.ts +10 -0
  50. package/dist/types/validation/numeric.d.ts +8 -0
  51. package/dist/types/validation/object.d.ts +13 -0
  52. package/dist/types/validation/ref.d.ts +11 -0
  53. package/dist/types/validation/shared.d.ts +26 -0
  54. package/dist/types/validation/string.d.ts +9 -0
  55. package/dist/types/validation/type.d.ts +6 -0
  56. package/dist/types/z-schema-base.d.ts +39 -1
  57. package/dist/types/z-schema-options.d.ts +3 -0
  58. package/dist/types/z-schema.d.ts +144 -8
  59. package/dist/utils/array.js +49 -7
  60. package/dist/utils/base64.js +29 -0
  61. package/dist/utils/clone.js +13 -12
  62. package/dist/utils/date.js +21 -0
  63. package/dist/utils/hostname.js +146 -0
  64. package/dist/utils/json.js +11 -6
  65. package/dist/utils/properties.js +1 -6
  66. package/dist/utils/time.js +50 -0
  67. package/dist/utils/unicode.js +8 -41
  68. package/dist/utils/uri.js +1 -1
  69. package/dist/validation/array.js +128 -0
  70. package/dist/validation/combinators.js +107 -0
  71. package/dist/validation/numeric.js +97 -0
  72. package/dist/validation/object.js +238 -0
  73. package/dist/validation/ref.js +70 -0
  74. package/dist/validation/shared.js +136 -0
  75. package/dist/validation/string.js +178 -0
  76. package/dist/validation/type.js +55 -0
  77. package/dist/z-schema-base.js +52 -32
  78. package/dist/z-schema-options.js +12 -8
  79. package/dist/z-schema-versions.js +92 -9
  80. package/dist/z-schema.js +135 -38
  81. package/package.json +22 -8
  82. package/src/errors.ts +8 -0
  83. package/src/format-validators.ts +146 -105
  84. package/src/index.ts +10 -1
  85. package/src/json-schema-versions.ts +181 -11
  86. package/src/json-schema.ts +102 -35
  87. package/src/json-validation.ts +653 -724
  88. package/src/report.ts +42 -20
  89. package/src/schema-cache.ts +94 -18
  90. package/src/schema-compiler.ts +94 -51
  91. package/src/schema-validator.ts +132 -56
  92. package/src/schemas/draft-07-schema.json +172 -0
  93. package/src/schemas/draft-2019-09-meta-applicator.json +53 -0
  94. package/src/schemas/draft-2019-09-meta-content.json +14 -0
  95. package/src/schemas/draft-2019-09-meta-core.json +54 -0
  96. package/src/schemas/draft-2019-09-meta-format.json +11 -0
  97. package/src/schemas/draft-2019-09-meta-meta-data.json +34 -0
  98. package/src/schemas/draft-2019-09-meta-validation.json +95 -0
  99. package/src/schemas/draft-2019-09-schema.json +42 -0
  100. package/src/schemas/draft-2020-12-meta-applicator.json +45 -0
  101. package/src/schemas/draft-2020-12-meta-content.json +14 -0
  102. package/src/schemas/draft-2020-12-meta-core.json +48 -0
  103. package/src/schemas/draft-2020-12-meta-format-annotation.json +11 -0
  104. package/src/schemas/draft-2020-12-meta-format-assertion.json +11 -0
  105. package/src/schemas/draft-2020-12-meta-meta-data.json +34 -0
  106. package/src/schemas/draft-2020-12-meta-unevaluated.json +12 -0
  107. package/src/schemas/draft-2020-12-meta-validation.json +95 -0
  108. package/src/schemas/draft-2020-12-schema.json +58 -0
  109. package/src/utils/array.ts +51 -7
  110. package/src/utils/base64.ts +32 -0
  111. package/src/utils/clone.ts +16 -12
  112. package/src/utils/date.ts +23 -0
  113. package/src/utils/hostname.ts +174 -0
  114. package/src/utils/json.ts +15 -6
  115. package/src/utils/properties.ts +1 -7
  116. package/src/utils/time.ts +73 -0
  117. package/src/utils/unicode.ts +8 -39
  118. package/src/utils/uri.ts +1 -1
  119. package/src/validation/array.ts +158 -0
  120. package/src/validation/combinators.ts +132 -0
  121. package/src/validation/numeric.ts +120 -0
  122. package/src/validation/object.ts +318 -0
  123. package/src/validation/ref.ts +85 -0
  124. package/src/validation/shared.ts +191 -0
  125. package/src/validation/string.ts +224 -0
  126. package/src/validation/type.ts +66 -0
  127. package/src/z-schema-base.ts +54 -36
  128. package/src/z-schema-options.ts +15 -8
  129. package/src/z-schema-versions.ts +107 -12
  130. package/src/z-schema.ts +158 -42
  131. package/umd/ZSchema.js +4446 -1685
  132. package/umd/ZSchema.min.js +1 -1
  133. package/dist/schemas/draft-04-hyper-schema.json +0 -135
  134. package/dist/schemas/draft-06-hyper-schema.json +0 -132
  135. package/dist/schemas/draft-06-links.json +0 -43
  136. package/src/schemas/draft-04-hyper-schema.json +0 -136
  137. package/src/schemas/draft-06-hyper-schema.json +0 -133
  138. package/src/schemas/draft-06-links.json +0 -43
@@ -1,698 +1,461 @@
1
- import { getFormatValidators } from './format-validators.js';
1
+ import { getId } from './json-schema.js';
2
2
  import { Report } from './report.js';
3
- import { difference, isUniqueArray } from './utils/array.js';
4
- import { shallowClone } from './utils/clone.js';
5
- import { areEqual } from './utils/json.js';
6
- import { hasOwn } from './utils/properties.js';
7
3
  import { compileSchemaRegex } from './utils/schema-regex.js';
8
- import { ucs2decode } from './utils/unicode.js';
9
4
  import { isObject, whatIs } from './utils/what-is.js';
10
- const shouldSkipValidate = function (options, errors) {
11
- return (options &&
12
- Array.isArray(options.includeErrors) &&
13
- options.includeErrors.length > 0 &&
14
- !errors.some(function (err) {
15
- return options.includeErrors.includes(err);
16
- }));
17
- };
18
- export const JsonValidators = {
19
- id: () => { },
20
- $ref: () => { },
21
- $schema: () => { },
22
- title: () => { },
23
- description: () => { },
24
- default: () => { },
25
- multipleOf: function (report, schema, json) {
26
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.1.1.2
27
- if (shouldSkipValidate(this.validateOptions, ['MULTIPLE_OF'])) {
28
- return;
29
- }
30
- if (typeof json !== 'number') {
31
- return;
32
- }
33
- const result = json / schema.multipleOf;
34
- if (!Number.isFinite(result) || Math.abs(result - Math.round(result)) >= 1e-10) {
35
- report.addError('MULTIPLE_OF', [json, schema.multipleOf], undefined, schema, 'multipleOf');
36
- }
37
- },
38
- maximum: function (report, schema, json) {
39
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.1.2.2
40
- if (shouldSkipValidate(this.validateOptions, ['MAXIMUM', 'MAXIMUM_EXCLUSIVE'])) {
41
- return;
42
- }
43
- if (typeof json !== 'number') {
44
- return;
45
- }
46
- if (schema.exclusiveMaximum !== true) {
47
- if (json > schema.maximum) {
48
- report.addError('MAXIMUM', [json, schema.maximum], undefined, schema, 'maximum');
49
- }
5
+ import { additionalItemsValidator, containsValidator, itemsValidator, maxContainsValidator, maxItemsValidator, minContainsValidator, minItemsValidator, prefixItemsValidator, uniqueItemsValidator, } from './validation/array.js';
6
+ import { allOfValidator, anyOfValidator, elseValidator, ifValidator, notValidator, oneOfValidator, thenValidator, } from './validation/combinators.js';
7
+ import { exclusiveMaximumValidator, exclusiveMinimumValidator, maximumValidator, minimumValidator, multipleOfValidator, } from './validation/numeric.js';
8
+ import { additionalPropertiesValidator, dependenciesValidator, dependentRequiredValidator, dependentSchemasValidator, maxPropertiesValidator, minPropertiesValidator, patternPropertiesValidator, propertiesValidator, propertyNamesValidator, requiredValidator, } from './validation/object.js';
9
+ import { resolveDynamicRef, resolveRecursiveRef } from './validation/ref.js';
10
+ import { getCachedValidationResult, isValidationVocabularyEnabled, VALIDATION_VOCAB_KEYWORDS, } from './validation/shared.js';
11
+ import { contentEncodingValidator, contentMediaTypeValidator, formatValidator, maxLengthValidator, minLengthValidator, patternValidator, } from './validation/string.js';
12
+ import { constValidator, enumValidator, typeValidator } from './validation/type.js';
13
+ function collectEvaluated(args) {
14
+ const { report, currentSchema, json, mode, depth } = args;
15
+ if (!currentSchema || typeof currentSchema === 'boolean') {
16
+ return new Set();
17
+ }
18
+ if (depth > (this.options.maxRecursionDepth ?? 100)) {
19
+ report.addError('COLLECT_EVALUATED_DEPTH_EXCEEDED', [depth]);
20
+ return new Set();
21
+ }
22
+ const evaluated = new Set();
23
+ const merge = (other) => {
24
+ if (other === 'all')
25
+ return true;
26
+ for (const v of other) {
27
+ evaluated.add(v);
50
28
  }
51
- else {
52
- if (json >= schema.maximum) {
53
- report.addError('MAXIMUM_EXCLUSIVE', [json, schema.maximum], undefined, schema, 'maximum');
54
- }
29
+ return false;
30
+ };
31
+ const recurse = (subSchema) => {
32
+ if (mode === 'items') {
33
+ return collectEvaluated.call(this, {
34
+ report,
35
+ currentSchema: subSchema,
36
+ json,
37
+ mode: 'items',
38
+ jsonArr: args.jsonArr,
39
+ depth: depth + 1,
40
+ });
55
41
  }
56
- },
57
- exclusiveMaximum: function (report, schema, json) {
58
- // In draft-06+, exclusiveMaximum is a standalone number
59
- if (typeof schema.exclusiveMaximum === 'number') {
60
- if (shouldSkipValidate(this.validateOptions, ['MAXIMUM_EXCLUSIVE'])) {
61
- return;
62
- }
63
- if (typeof json !== 'number') {
64
- return;
42
+ return collectEvaluated.call(this, {
43
+ report,
44
+ currentSchema: subSchema,
45
+ json,
46
+ mode: 'properties',
47
+ jsonData: args.jsonData,
48
+ depth: depth + 1,
49
+ });
50
+ };
51
+ // --- Mode-specific leaf collection ---
52
+ if (mode === 'items') {
53
+ const jsonArr = args.jsonArr;
54
+ // prefixItems (2020-12 tuple)
55
+ if (Array.isArray(currentSchema.prefixItems)) {
56
+ const len = Math.min(currentSchema.prefixItems.length, jsonArr.length);
57
+ for (let i = 0; i < len; i++) {
58
+ evaluated.add(i);
59
+ }
60
+ }
61
+ // items - can be array (2019-09 tuple) or schema (evaluates all)
62
+ if (currentSchema.items !== undefined) {
63
+ if (Array.isArray(currentSchema.items)) {
64
+ const len = Math.min(currentSchema.items.length, jsonArr.length);
65
+ for (let i = 0; i < len; i++) {
66
+ evaluated.add(i);
67
+ }
65
68
  }
66
- if (json >= schema.exclusiveMaximum) {
67
- report.addError('MAXIMUM_EXCLUSIVE', [json, schema.exclusiveMaximum], undefined, schema, 'exclusiveMaximum');
69
+ else if (currentSchema.items !== false) {
70
+ return 'all';
68
71
  }
69
72
  }
70
- // In draft-04, exclusiveMaximum is a boolean handled inside the `maximum` validator
71
- },
72
- minimum: function (report, schema, json) {
73
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.1.3.2
74
- if (shouldSkipValidate(this.validateOptions, ['MINIMUM', 'MINIMUM_EXCLUSIVE'])) {
75
- return;
76
- }
77
- if (typeof json !== 'number') {
78
- return;
73
+ // additionalItems (2019-09) - when items is array form and additionalItems is present and not false
74
+ if (currentSchema.additionalItems !== undefined &&
75
+ currentSchema.additionalItems !== false &&
76
+ Array.isArray(currentSchema.items)) {
77
+ return 'all';
79
78
  }
80
- if (schema.exclusiveMinimum !== true) {
81
- if (json < schema.minimum) {
82
- report.addError('MINIMUM', [json, schema.minimum], undefined, schema, 'minimum');
83
- }
84
- }
85
- else {
86
- if (json <= schema.minimum) {
87
- report.addError('MINIMUM_EXCLUSIVE', [json, schema.minimum], undefined, schema, 'minimum');
88
- }
89
- }
90
- },
91
- exclusiveMinimum: function (report, schema, json) {
92
- // In draft-06+, exclusiveMinimum is a standalone number
93
- if (typeof schema.exclusiveMinimum === 'number') {
94
- if (shouldSkipValidate(this.validateOptions, ['MINIMUM_EXCLUSIVE'])) {
95
- return;
96
- }
97
- if (typeof json !== 'number') {
98
- return;
79
+ // contains - evaluates specific indices that match the schema
80
+ if (currentSchema.contains !== undefined) {
81
+ for (let i = 0; i < jsonArr.length; i++) {
82
+ let passed = getCachedValidationResult(report, currentSchema.contains, jsonArr[i]);
83
+ if (passed === undefined) {
84
+ const subReport = new Report(report);
85
+ validate.call(this, subReport, currentSchema.contains, jsonArr[i]);
86
+ passed = subReport.errors.length === 0;
87
+ }
88
+ if (passed) {
89
+ evaluated.add(i);
90
+ }
99
91
  }
100
- if (json <= schema.exclusiveMinimum) {
101
- report.addError('MINIMUM_EXCLUSIVE', [json, schema.exclusiveMinimum], undefined, schema, 'exclusiveMinimum');
102
- }
103
- }
104
- // In draft-04, exclusiveMinimum is a boolean handled inside the `minimum` validator
105
- },
106
- maxLength: function (report, schema, json) {
107
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.2.1.2
108
- if (shouldSkipValidate(this.validateOptions, ['MAX_LENGTH'])) {
109
- return;
110
- }
111
- if (typeof json !== 'string') {
112
- return;
113
- }
114
- if (ucs2decode(json).length > schema.maxLength) {
115
- report.addError('MAX_LENGTH', [json.length, schema.maxLength], undefined, schema, 'maxLength');
116
92
  }
117
- },
118
- minLength: function (report, schema, json) {
119
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.2.2.2
120
- if (shouldSkipValidate(this.validateOptions, ['MIN_LENGTH'])) {
121
- return;
122
- }
123
- if (typeof json !== 'string') {
124
- return;
125
- }
126
- if (ucs2decode(json).length < schema.minLength) {
127
- report.addError('MIN_LENGTH', [json.length, schema.minLength], undefined, schema, 'minLength');
93
+ // unevaluatedItems: true means all items evaluated
94
+ if (currentSchema.unevaluatedItems === true) {
95
+ return 'all';
128
96
  }
129
- },
130
- pattern: function (report, schema, json) {
131
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.2.3.2
132
- if (shouldSkipValidate(this.validateOptions, ['PATTERN'])) {
133
- return;
134
- }
135
- if (typeof json !== 'string') {
136
- return;
137
- }
138
- const result = compileSchemaRegex(schema.pattern);
139
- if (!result.ok) {
140
- // Should not happen: schema should have been validated already
141
- report.addError('PATTERN', [schema.pattern, json, result.error.message], undefined, schema, 'pattern');
142
- return;
143
- }
144
- if (!result.value.test(json)) {
145
- report.addError('PATTERN', [schema.pattern, json], undefined, schema, 'pattern');
146
- }
147
- },
148
- additionalItems: function (report, schema, json) {
149
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.3.1.2
150
- if (shouldSkipValidate(this.validateOptions, ['ARRAY_ADDITIONAL_ITEMS'])) {
151
- return;
152
- }
153
- if (!Array.isArray(json)) {
154
- return;
155
- }
156
- // if the value of "additionalItems" is boolean value false and the value of "items" is an array,
157
- // the json is valid if its size is less than, or equal to, the size of "items".
158
- if (schema.additionalItems === false && Array.isArray(schema.items)) {
159
- if (json.length > schema.items.length) {
160
- report.addError('ARRAY_ADDITIONAL_ITEMS', undefined, undefined, schema, 'additionalItems');
161
- }
162
- }
163
- },
164
- items: function () {
165
- /*report: Report, schema: JsonSchemaInternal, json: unknown*/
166
- // covered in additionalItems
167
- },
168
- maxItems: function (report, schema, json) {
169
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.3.2.2
170
- if (shouldSkipValidate(this.validateOptions, ['ARRAY_LENGTH_LONG'])) {
171
- return;
172
- }
173
- if (!Array.isArray(json)) {
174
- return;
175
- }
176
- if (json.length > schema.maxItems) {
177
- report.addError('ARRAY_LENGTH_LONG', [json.length, schema.maxItems], undefined, schema, 'maxItems');
178
- }
179
- },
180
- minItems: function (report, schema, json) {
181
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.3.3.2
182
- if (shouldSkipValidate(this.validateOptions, ['ARRAY_LENGTH_SHORT'])) {
183
- return;
184
- }
185
- if (!Array.isArray(json)) {
186
- return;
187
- }
188
- if (json.length < schema.minItems) {
189
- report.addError('ARRAY_LENGTH_SHORT', [json.length, schema.minItems], undefined, schema, 'minItems');
190
- }
191
- },
192
- uniqueItems: function (report, schema, json) {
193
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.3.4.2
194
- if (shouldSkipValidate(this.validateOptions, ['ARRAY_UNIQUE'])) {
195
- return;
196
- }
197
- if (!Array.isArray(json)) {
198
- return;
199
- }
200
- if (schema.uniqueItems === true) {
201
- const matches = [];
202
- if (isUniqueArray(json, matches) === false) {
203
- report.addError('ARRAY_UNIQUE', matches, undefined, schema, 'uniqueItems');
204
- }
205
- }
206
- },
207
- maxProperties: function (report, schema, json) {
208
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.4.1.2
209
- if (shouldSkipValidate(this.validateOptions, ['OBJECT_PROPERTIES_MAXIMUM'])) {
210
- return;
211
- }
212
- if (!isObject(json)) {
213
- return;
214
- }
215
- const keysCount = Object.keys(json).length;
216
- if (keysCount > schema.maxProperties) {
217
- report.addError('OBJECT_PROPERTIES_MAXIMUM', [keysCount, schema.maxProperties], undefined, schema, 'maxProperties');
218
- }
219
- },
220
- minProperties: function (report, schema, json) {
221
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.4.2.2
222
- if (shouldSkipValidate(this.validateOptions, ['OBJECT_PROPERTIES_MINIMUM'])) {
223
- return;
224
- }
225
- if (!isObject(json)) {
226
- return;
227
- }
228
- const keysCount = Object.keys(json).length;
229
- if (keysCount < schema.minProperties) {
230
- report.addError('OBJECT_PROPERTIES_MINIMUM', [keysCount, schema.minProperties], undefined, schema, 'minProperties');
231
- }
232
- },
233
- required: function (report, schema, json) {
234
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.4.3.2
235
- if (shouldSkipValidate(this.validateOptions, ['OBJECT_MISSING_REQUIRED_PROPERTY'])) {
236
- return;
237
- }
238
- if (!isObject(json)) {
239
- return;
240
- }
241
- let idx = schema.required.length;
242
- while (idx--) {
243
- const requiredPropertyName = schema.required[idx];
244
- if (!hasOwn(json, requiredPropertyName)) {
245
- report.addError('OBJECT_MISSING_REQUIRED_PROPERTY', [requiredPropertyName], undefined, schema, 'required');
246
- }
247
- }
248
- },
249
- additionalProperties: function (report, schema, json) {
250
- // covered in properties and patternProperties
251
- if (schema.properties === undefined && schema.patternProperties === undefined) {
252
- return JsonValidators.properties.call(this, report, schema, json);
253
- }
254
- },
255
- patternProperties: function (report, schema, json) {
256
- // covered in properties
257
- if (schema.properties === undefined) {
258
- return JsonValidators.properties.call(this, report, schema, json);
259
- }
260
- },
261
- properties: function (report, schema, json) {
262
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.4.4.2
263
- if (shouldSkipValidate(this.validateOptions, ['OBJECT_ADDITIONAL_PROPERTIES'])) {
264
- return;
265
- }
266
- if (!isObject(json)) {
267
- return;
268
- }
269
- const properties = schema.properties !== undefined ? schema.properties : {};
270
- const patternProperties = schema.patternProperties !== undefined ? schema.patternProperties : {};
271
- if (schema.additionalProperties === false) {
272
- // The property set of the json to validate.
273
- let s = Object.keys(json);
274
- // The property set from "properties".
275
- const p = Object.keys(properties);
276
- // The property set from "patternProperties".
277
- const pp = Object.keys(patternProperties);
278
- // remove from "s" all elements of "p", if any;
279
- s = difference(s, p);
280
- // for each regex in "pp", remove all elements of "s" which this regex matches.
281
- let idx = pp.length;
282
- while (idx--) {
283
- const result = compileSchemaRegex(pp[idx]);
284
- if (!result.ok) {
285
- continue;
286
- }
287
- const regExp = result.value;
288
- let idx2 = s.length;
289
- while (idx2--) {
290
- if (regExp.test(s[idx2]) === true) {
291
- s.splice(idx2, 1);
292
- }
97
+ }
98
+ else {
99
+ // mode === 'properties'
100
+ const jsonData = args.jsonData;
101
+ // properties
102
+ if (isObject(currentSchema.properties)) {
103
+ for (const key of Object.keys(currentSchema.properties)) {
104
+ if (Object.hasOwn(jsonData, key)) {
105
+ evaluated.add(key);
293
106
  }
294
107
  }
295
- // Validation of the json succeeds if, after these two steps, set "s" is empty.
296
- if (s.length > 0) {
297
- // assumeAdditional can be an array of allowed properties
298
- if (Array.isArray(this.options.assumeAdditional)) {
299
- let idx3 = this.options.assumeAdditional.length;
300
- if (idx3) {
301
- while (idx3--) {
302
- const io = s.indexOf(this.options.assumeAdditional[idx3]);
303
- if (io !== -1) {
304
- s.splice(io, 1);
305
- }
108
+ }
109
+ // patternProperties
110
+ if (isObject(currentSchema.patternProperties)) {
111
+ for (const pattern of Object.keys(currentSchema.patternProperties)) {
112
+ const result = compileSchemaRegex(pattern);
113
+ if (result.ok) {
114
+ for (const key of Object.keys(jsonData)) {
115
+ if (result.value.test(key)) {
116
+ evaluated.add(key);
306
117
  }
307
118
  }
308
119
  }
309
- let idx4 = s.length;
310
- if (idx4) {
311
- while (idx4--) {
312
- report.addError('OBJECT_ADDITIONAL_PROPERTIES', [s[idx4]], undefined, schema, 'properties');
313
- }
314
- }
315
120
  }
316
121
  }
317
- },
318
- dependencies: function (report, schema, json) {
319
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.4.5.2
320
- if (shouldSkipValidate(this.validateOptions, ['OBJECT_DEPENDENCY_KEY'])) {
321
- return;
322
- }
323
- if (!isObject(json)) {
324
- return;
325
- }
326
- const keys = Object.keys(schema.dependencies);
327
- let idx = keys.length;
328
- while (idx--) {
329
- // iterate all dependencies
330
- const dependencyName = keys[idx];
331
- if (hasOwn(json, dependencyName)) {
332
- const dependencyDefinition = schema.dependencies[dependencyName];
333
- if (Array.isArray(dependencyDefinition)) {
334
- // Array
335
- // if dependency is an array, object needs to have all properties in this array
336
- let idx2 = dependencyDefinition.length;
337
- while (idx2--) {
338
- const requiredPropertyName = dependencyDefinition[idx2];
339
- if (!hasOwn(json, requiredPropertyName)) {
340
- report.addError('OBJECT_DEPENDENCY_KEY', [requiredPropertyName, dependencyName], undefined, schema, 'dependencies');
341
- }
122
+ // additionalProperties - evaluates all non-properties/non-patternProperties keys
123
+ if (currentSchema.additionalProperties !== undefined) {
124
+ const propKeys = isObject(currentSchema.properties) ? Object.keys(currentSchema.properties) : [];
125
+ const patternRegexes = [];
126
+ if (isObject(currentSchema.patternProperties)) {
127
+ for (const pattern of Object.keys(currentSchema.patternProperties)) {
128
+ const result = compileSchemaRegex(pattern);
129
+ if (result.ok) {
130
+ patternRegexes.push(result.value);
342
131
  }
343
132
  }
344
- else {
345
- // if dependency is a schema, validate against this schema
346
- validate.call(this, report, dependencyDefinition, json);
347
- }
348
133
  }
349
- }
350
- },
351
- enum: function (report, schema, json) {
352
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.1.2
353
- if (shouldSkipValidate(this.validateOptions, ['ENUM_CASE_MISMATCH', 'ENUM_MISMATCH'])) {
354
- return;
355
- }
356
- let match = false, caseInsensitiveMatch = false, idx = schema.enum.length;
357
- while (idx--) {
358
- if (areEqual(json, schema.enum[idx])) {
359
- match = true;
360
- break;
361
- }
362
- else if (areEqual(json, schema.enum[idx], { caseInsensitiveComparison: true })) {
363
- caseInsensitiveMatch = true;
134
+ for (const key of Object.keys(jsonData)) {
135
+ if (propKeys.includes(key))
136
+ continue;
137
+ if (patternRegexes.some((re) => re.test(key)))
138
+ continue;
139
+ evaluated.add(key);
364
140
  }
365
141
  }
366
- if (match === false) {
367
- const error = caseInsensitiveMatch && this.options.enumCaseInsensitiveComparison ? 'ENUM_CASE_MISMATCH' : 'ENUM_MISMATCH';
368
- report.addError(error, [JSON.stringify(json)], undefined, schema, 'enum');
369
- }
370
- },
371
- type: function (report, schema, json) {
372
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.2.2
373
- if (shouldSkipValidate(this.validateOptions, ['INVALID_TYPE'])) {
374
- return;
375
- }
376
- const jsonType = whatIs(json);
377
- if (typeof schema.type === 'string') {
378
- if (jsonType !== schema.type && (jsonType !== 'integer' || schema.type !== 'number')) {
379
- report.addError('INVALID_TYPE', [schema.type, jsonType], undefined, schema, 'type');
142
+ // dependentSchemas - only applies when the dependency key is present in the data
143
+ if (isObject(currentSchema.dependentSchemas)) {
144
+ for (const [depKey, depSchema] of Object.entries(currentSchema.dependentSchemas)) {
145
+ if (Object.hasOwn(jsonData, depKey)) {
146
+ if (merge(recurse(depSchema))) {
147
+ return 'all';
148
+ }
149
+ }
380
150
  }
381
151
  }
382
- else {
383
- if (schema.type.indexOf(jsonType) === -1 && (jsonType !== 'integer' || schema.type.indexOf('number') === -1)) {
384
- report.addError('INVALID_TYPE', [JSON.stringify(schema.type), jsonType], undefined, schema, 'type');
385
- }
152
+ // unevaluatedProperties: true in a sub-schema means all props are evaluated
153
+ if (currentSchema.unevaluatedProperties === true) {
154
+ return 'all';
386
155
  }
387
- },
388
- allOf: function (report, schema, json) {
389
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.3.2
390
- let idx = schema.allOf.length;
391
- while (idx--) {
392
- const validateResult = validate.call(this, report, schema.allOf[idx], json);
393
- if (this.options.breakOnFirstError && validateResult === false) {
394
- break;
156
+ }
157
+ // --- Shared combinator traversal ---
158
+ // allOf
159
+ if (Array.isArray(currentSchema.allOf)) {
160
+ for (const subSchema of currentSchema.allOf) {
161
+ if (merge(recurse(subSchema))) {
162
+ return 'all';
395
163
  }
396
164
  }
397
- },
398
- anyOf: function (report, schema, json) {
399
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.4.2
400
- const subReports = [];
401
- let idx = schema.anyOf.length;
402
- while (idx--) {
403
- const subReport = new Report(report);
404
- subReports.push(subReport);
405
- validate.call(this, subReport, schema.anyOf[idx], json);
406
- }
407
- // Aggregate async tasks from sub-reports to the main report
408
- const asyncTasksBefore = report.asyncTasks.length;
409
- for (const subReport of subReports) {
410
- report.asyncTasks.push(...subReport.asyncTasks);
411
- }
412
- const hasAsyncTasks = report.asyncTasks.length > asyncTasksBefore;
413
- if (hasAsyncTasks) {
414
- // Defer the decision until async tasks complete
415
- const pathBeforeAsync = shallowClone(report.path);
416
- report.addAsyncTask((callback) => {
417
- setTimeout(() => callback(null), 0);
418
- }, [], () => {
419
- const backup = report.path;
420
- report.path = pathBeforeAsync;
421
- let passed = false;
422
- for (const subReport of subReports) {
423
- if (subReport.errors.length === 0) {
424
- passed = true;
425
- break;
426
- }
427
- }
428
- if (passed === false) {
429
- report.addError('ANY_OF_MISSING', undefined, subReports, schema, 'anyOf');
165
+ }
166
+ // anyOf - only matching branches contribute
167
+ if (Array.isArray(currentSchema.anyOf)) {
168
+ for (const subSchema of currentSchema.anyOf) {
169
+ let passed = getCachedValidationResult(report, subSchema, json);
170
+ if (passed === undefined) {
171
+ const subReport = new Report(report);
172
+ validate.call(this, subReport, subSchema, json);
173
+ passed = subReport.errors.length === 0;
174
+ }
175
+ if (passed) {
176
+ if (merge(recurse(subSchema))) {
177
+ return 'all';
430
178
  }
431
- report.path = backup;
432
- });
179
+ }
433
180
  }
434
- else {
435
- // No async tasks, decide immediately
436
- let passed = false;
437
- for (const subReport of subReports) {
438
- if (subReport.errors.length === 0) {
439
- passed = true;
440
- break;
181
+ }
182
+ // oneOf - only matching branches contribute
183
+ if (Array.isArray(currentSchema.oneOf)) {
184
+ for (const subSchema of currentSchema.oneOf) {
185
+ let passed = getCachedValidationResult(report, subSchema, json);
186
+ if (passed === undefined) {
187
+ const subReport = new Report(report);
188
+ validate.call(this, subReport, subSchema, json);
189
+ passed = subReport.errors.length === 0;
190
+ }
191
+ if (passed) {
192
+ if (merge(recurse(subSchema))) {
193
+ return 'all';
441
194
  }
442
195
  }
443
- if (passed === false) {
444
- report.addError('ANY_OF_MISSING', undefined, subReports, schema, 'anyOf');
445
- }
446
196
  }
447
- },
448
- oneOf: function (report, schema, json) {
449
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.5.2
450
- const subReports = [];
451
- let idx = schema.oneOf.length;
452
- while (idx--) {
453
- const subReport = new Report(report);
454
- subReports.push(subReport);
455
- validate.call(this, subReport, schema.oneOf[idx], json);
456
- }
457
- // Aggregate async tasks from sub-reports to the main report
458
- const asyncTasksBefore = report.asyncTasks.length;
459
- for (const subReport of subReports) {
460
- report.asyncTasks.push(...subReport.asyncTasks);
461
- }
462
- const hasAsyncTasks = report.asyncTasks.length > asyncTasksBefore;
463
- if (hasAsyncTasks) {
464
- // Defer the decision until async tasks complete
465
- const pathBeforeAsync = shallowClone(report.path);
466
- report.addAsyncTask((callback) => {
467
- // This task runs after all async tasks, so we can check final state
468
- setTimeout(() => callback(null), 0);
469
- }, [], () => {
470
- const backup = report.path;
471
- report.path = pathBeforeAsync;
472
- let passes = 0;
473
- for (const subReport of subReports) {
474
- if (subReport.errors.length === 0) {
475
- passes++;
476
- }
477
- }
478
- if (passes === 0) {
479
- report.addError('ONE_OF_MISSING', undefined, subReports, schema, 'oneOf');
480
- }
481
- else if (passes > 1) {
482
- report.addError('ONE_OF_MULTIPLE', undefined, undefined, schema, 'oneOf');
197
+ }
198
+ // if/then/else
199
+ if (currentSchema.if !== undefined) {
200
+ let condPassed = getCachedValidationResult(report, currentSchema.if, json);
201
+ if (condPassed === undefined) {
202
+ const condReport = new Report(report);
203
+ validate.call(this, condReport, currentSchema.if, json);
204
+ condPassed = condReport.errors.length === 0;
205
+ }
206
+ if (condPassed) {
207
+ if (merge(recurse(currentSchema.if))) {
208
+ return 'all';
209
+ }
210
+ if (currentSchema.then !== undefined) {
211
+ if (merge(recurse(currentSchema.then))) {
212
+ return 'all';
483
213
  }
484
- report.path = backup;
485
- });
214
+ }
486
215
  }
487
216
  else {
488
- // No async tasks, decide immediately
489
- let passes = 0;
490
- for (const subReport of subReports) {
491
- if (subReport.errors.length === 0) {
492
- passes++;
493
- }
494
- }
495
- if (passes === 0) {
496
- report.addError('ONE_OF_MISSING', undefined, subReports, schema, 'oneOf');
497
- }
498
- else if (passes > 1) {
499
- report.addError('ONE_OF_MULTIPLE', undefined, undefined, schema, 'oneOf');
500
- }
501
- }
502
- },
503
- not: function (report, schema, json) {
504
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.6.2
505
- const subReport = new Report(report);
506
- if (validate.call(this, subReport, schema.not, json) === true) {
507
- report.addError('NOT_PASSED', undefined, undefined, schema, 'not');
508
- }
509
- },
510
- definitions: function () {
511
- /*report: Report, schema: JsonSchemaInternal, json: unknown*/
512
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.7.2
513
- // nothing to do here
514
- },
515
- format: function (report, schema, json) {
516
- // http://json-schema.org/latest/json-schema-validation.html#rfc.section.7.2
517
- const formatValidators = getFormatValidators(this.options);
518
- const formatValidatorFn = formatValidators[schema.format];
519
- if (typeof formatValidatorFn === 'function') {
520
- if (shouldSkipValidate(this.validateOptions, ['INVALID_FORMAT'])) {
521
- return;
522
- }
523
- if (report.hasError('INVALID_TYPE', [schema.type, whatIs(json)])) {
524
- return;
525
- }
526
- if (formatValidatorFn.length === 2) {
527
- // callback-based async - need to clone the path here, because it will change by the time async function reports back
528
- const pathBeforeAsync = shallowClone(report.path);
529
- report.addAsyncTask(formatValidatorFn, [json], function (result) {
530
- if (result !== true) {
531
- const backup = report.path;
532
- report.path = pathBeforeAsync;
533
- report.addError('INVALID_FORMAT', [schema.format, JSON.stringify(json)], undefined, schema, 'format');
534
- report.path = backup;
535
- }
536
- });
537
- }
538
- else {
539
- const result = formatValidatorFn.call(this, json);
540
- if (result instanceof Promise) {
541
- // Promise-based async
542
- const pathBeforeAsync = shallowClone(report.path);
543
- const timeoutMs = this.options.asyncTimeout || 2000;
544
- report.addAsyncTask(async (callback) => {
545
- try {
546
- const timeoutPromise = new Promise((_, reject) => {
547
- setTimeout(() => reject(new Error('Async timeout')), timeoutMs);
548
- });
549
- const resolved = await Promise.race([result, timeoutPromise]);
550
- callback(resolved);
551
- }
552
- catch (error) {
553
- if (error.message === 'Async timeout') {
554
- // Don't call callback, let global timeout handle it
555
- return;
556
- }
557
- callback(false);
558
- }
559
- }, [], function (resolvedResult) {
560
- if (resolvedResult !== true) {
561
- const backup = report.path;
562
- report.path = pathBeforeAsync;
563
- report.addError('INVALID_FORMAT', [schema.format, JSON.stringify(json)], undefined, schema, 'format');
564
- report.path = backup;
565
- }
566
- });
567
- }
568
- else {
569
- // sync
570
- if (result !== true) {
571
- report.addError('INVALID_FORMAT', [schema.format, JSON.stringify(json)], undefined, schema, 'format');
572
- }
217
+ if (currentSchema.else !== undefined) {
218
+ if (merge(recurse(currentSchema.else))) {
219
+ return 'all';
573
220
  }
574
221
  }
575
222
  }
576
- else if (this.options.ignoreUnknownFormats !== true) {
577
- report.addError('UNKNOWN_FORMAT', [schema.format], undefined, schema, 'format');
578
- }
579
- },
580
- // draft-06 additions
581
- $id: () => {
582
- // TODO: implement
583
- },
584
- const: function (report, schema, json) {
585
- const constValue = schema.const;
586
- if (areEqual(json, constValue) === false) {
587
- report.addError('CONST', [JSON.stringify(constValue)], undefined, schema, undefined);
223
+ }
224
+ // $ref resolved
225
+ if (currentSchema.__$refResolved && currentSchema.__$refResolved !== currentSchema) {
226
+ if (merge(recurse(currentSchema.__$refResolved))) {
227
+ return 'all';
588
228
  }
589
- },
590
- contains: function (report, schema, json) {
591
- if (shouldSkipValidate(this.validateOptions, ['CONTAINS'])) {
592
- return;
229
+ }
230
+ // $recursiveRef
231
+ const recursiveTarget = resolveRecursiveRef(currentSchema, report.__$recursiveAnchorStack);
232
+ if (recursiveTarget && recursiveTarget !== currentSchema) {
233
+ if (merge(recurse(recursiveTarget))) {
234
+ return 'all';
593
235
  }
594
- if (!Array.isArray(json)) {
595
- return;
236
+ }
237
+ // $dynamicRef
238
+ const dynamicTarget = resolveDynamicRef(currentSchema, report.__$dynamicScopeStack);
239
+ if (dynamicTarget && dynamicTarget !== currentSchema) {
240
+ if (merge(recurse(dynamicTarget))) {
241
+ return 'all';
596
242
  }
597
- const containsSchema = schema.contains;
598
- if (containsSchema === undefined) {
599
- return;
243
+ }
244
+ return evaluated;
245
+ }
246
+ // ---------------------------------------------------------------------------
247
+ // unevaluatedItems
248
+ // ---------------------------------------------------------------------------
249
+ function unevaluatedItemsValidator(report, schema, json) {
250
+ if (!Array.isArray(json)) {
251
+ return;
252
+ }
253
+ // unevaluatedItems: true means all items are valid
254
+ if (schema.unevaluatedItems === true) {
255
+ return;
256
+ }
257
+ const unevalSchema = schema.unevaluatedItems;
258
+ if (unevalSchema === undefined) {
259
+ return;
260
+ }
261
+ if (json.length === 0) {
262
+ return;
263
+ }
264
+ const evaluatedItems = collectEvaluated.call(this, {
265
+ report,
266
+ currentSchema: schema,
267
+ json,
268
+ mode: 'items',
269
+ jsonArr: json,
270
+ depth: 0,
271
+ });
272
+ if (evaluatedItems === 'all') {
273
+ return;
274
+ }
275
+ const unevaluatedIndices = [];
276
+ for (let i = 0; i < json.length; i++) {
277
+ if (!evaluatedItems.has(i)) {
278
+ unevaluatedIndices.push(i);
600
279
  }
601
- const subReports = [];
602
- let idx = json.length;
603
- while (idx--) {
280
+ }
281
+ if (unevaluatedIndices.length === 0) {
282
+ return;
283
+ }
284
+ if (unevalSchema === false) {
285
+ report.addError('ARRAY_UNEVALUATED_ITEMS', undefined, undefined, schema, 'unevaluatedItems');
286
+ }
287
+ else {
288
+ // unevaluatedItems as a schema — validate each unevaluated item against it
289
+ for (const idx of unevaluatedIndices) {
604
290
  const subReport = new Report(report);
605
- subReports.push(subReport);
606
- validate.call(this, subReport, containsSchema, json[idx]);
607
- }
608
- const asyncTasksBefore = report.asyncTasks.length;
609
- for (const subReport of subReports) {
610
- report.asyncTasks.push(...subReport.asyncTasks);
611
- }
612
- const hasAsyncTasks = report.asyncTasks.length > asyncTasksBefore;
613
- const addContainsErrorIfNeeded = () => {
614
- let hasValidItem = false;
615
- for (const subReport of subReports) {
616
- if (subReport.errors.length === 0) {
617
- hasValidItem = true;
618
- break;
619
- }
291
+ validate.call(this, subReport, unevalSchema, json[idx]);
292
+ if (subReport.errors.length > 0) {
293
+ report.addError('ARRAY_UNEVALUATED_ITEMS', undefined, undefined, schema, 'unevaluatedItems');
294
+ break;
620
295
  }
621
- if (!hasValidItem) {
622
- report.addError('CONTAINS', undefined, subReports, schema, undefined);
623
- }
624
- };
625
- if (hasAsyncTasks) {
626
- const pathBeforeAsync = shallowClone(report.path);
627
- report.addAsyncTask((callback) => {
628
- setTimeout(() => callback(null), 0);
629
- }, [], () => {
630
- const backup = report.path;
631
- report.path = pathBeforeAsync;
632
- addContainsErrorIfNeeded();
633
- report.path = backup;
634
- });
635
- return;
636
- }
637
- addContainsErrorIfNeeded();
638
- },
639
- examples: () => {
640
- // TODO: implement
641
- },
642
- propertyNames: function (report, schema, json) {
643
- if (shouldSkipValidate(this.validateOptions, ['PROPERTY_NAMES'])) {
644
- return;
645
- }
646
- if (!isObject(json)) {
647
- return;
648
- }
649
- const propertyNamesSchema = schema.propertyNames;
650
- if (propertyNamesSchema === undefined) {
651
- return;
652
- }
653
- const keys = Object.keys(json);
654
- const subReports = [];
655
- for (const key of keys) {
296
+ }
297
+ }
298
+ }
299
+ // ---------------------------------------------------------------------------
300
+ // unevaluatedProperties
301
+ // ---------------------------------------------------------------------------
302
+ function unevaluatedPropertiesValidator(report, schema, json) {
303
+ if (!isObject(json)) {
304
+ return;
305
+ }
306
+ // unevaluatedProperties: true means all properties are valid
307
+ if (schema.unevaluatedProperties === true) {
308
+ return;
309
+ }
310
+ // unevaluatedProperties: false or unevaluatedProperties: {schema} both need evaluation
311
+ const unevalSchema = schema.unevaluatedProperties;
312
+ if (unevalSchema === undefined) {
313
+ return;
314
+ }
315
+ const allKeys = Object.keys(json);
316
+ if (allKeys.length === 0) {
317
+ return;
318
+ }
319
+ const evaluatedProperties = collectEvaluated.call(this, {
320
+ report,
321
+ currentSchema: schema,
322
+ json,
323
+ mode: 'properties',
324
+ jsonData: json,
325
+ depth: 0,
326
+ });
327
+ if (evaluatedProperties === 'all') {
328
+ return;
329
+ }
330
+ const unevaluatedKeys = allKeys.filter((key) => !evaluatedProperties.has(key));
331
+ if (unevaluatedKeys.length === 0) {
332
+ return;
333
+ }
334
+ if (unevalSchema === false) {
335
+ report.addError('OBJECT_UNEVALUATED_PROPERTIES', [unevaluatedKeys.join(', ')], undefined, schema, 'unevaluatedProperties');
336
+ }
337
+ else {
338
+ // unevaluatedProperties as a schema — validate each unevaluated key against it
339
+ for (const key of unevaluatedKeys) {
656
340
  const subReport = new Report(report);
657
- subReports.push(subReport);
658
- validate.call(this, subReport, propertyNamesSchema, key);
659
- }
660
- const asyncTasksBefore = report.asyncTasks.length;
661
- for (const subReport of subReports) {
662
- report.asyncTasks.push(...subReport.asyncTasks);
663
- }
664
- const hasAsyncTasks = report.asyncTasks.length > asyncTasksBefore;
665
- const addPropertyNameErrors = () => {
666
- for (let idx = 0; idx < keys.length; idx++) {
667
- if (subReports[idx].errors.length > 0) {
668
- report.addError('PROPERTY_NAMES', [keys[idx]], subReports[idx], schema, undefined);
669
- }
341
+ validate.call(this, subReport, unevalSchema, json[key]);
342
+ if (subReport.errors.length > 0) {
343
+ report.addError('OBJECT_UNEVALUATED_PROPERTIES', [key], undefined, schema, 'unevaluatedProperties');
670
344
  }
671
- };
672
- if (hasAsyncTasks) {
673
- const pathBeforeAsync = shallowClone(report.path);
674
- report.addAsyncTask((callback) => {
675
- setTimeout(() => callback(null), 0);
676
- }, [], () => {
677
- const backup = report.path;
678
- report.path = pathBeforeAsync;
679
- addPropertyNameErrors();
680
- report.path = backup;
681
- });
682
- return;
683
345
  }
684
- addPropertyNameErrors();
685
- },
346
+ }
347
+ }
348
+ // ---------------------------------------------------------------------------
349
+ // definitions (no-op)
350
+ // ---------------------------------------------------------------------------
351
+ function definitionsValidator() {
352
+ /*report: Report, schema: JsonSchemaInternal, json: unknown*/
353
+ // http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.5.7.2
354
+ // nothing to do here
355
+ }
356
+ // ---------------------------------------------------------------------------
357
+ // JsonValidators — keyword dispatch table
358
+ // ---------------------------------------------------------------------------
359
+ export const JsonValidators = {
360
+ // no-op validators (metadata / handled elsewhere)
361
+ id: () => { },
362
+ $id: () => { },
363
+ $ref: () => { },
364
+ $schema: () => { },
365
+ $dynamicAnchor: () => { },
366
+ $dynamicRef: () => { },
367
+ $anchor: () => { },
368
+ $defs: () => { },
369
+ $vocabulary: () => { },
370
+ $recursiveAnchor: () => { },
371
+ $recursiveRef: () => { },
372
+ examples: () => { },
373
+ title: () => { },
374
+ description: () => { },
375
+ default: () => { },
376
+ // type validators
377
+ type: typeValidator,
378
+ enum: enumValidator,
379
+ const: constValidator,
380
+ // numeric validators
381
+ multipleOf: multipleOfValidator,
382
+ maximum: maximumValidator,
383
+ exclusiveMaximum: exclusiveMaximumValidator,
384
+ minimum: minimumValidator,
385
+ exclusiveMinimum: exclusiveMinimumValidator,
386
+ // string validators
387
+ maxLength: maxLengthValidator,
388
+ minLength: minLengthValidator,
389
+ pattern: patternValidator,
390
+ format: formatValidator,
391
+ contentEncoding: contentEncodingValidator,
392
+ contentMediaType: contentMediaTypeValidator,
393
+ // array validators
394
+ additionalItems: additionalItemsValidator,
395
+ items: itemsValidator,
396
+ prefixItems: prefixItemsValidator,
397
+ maxItems: maxItemsValidator,
398
+ minItems: minItemsValidator,
399
+ uniqueItems: uniqueItemsValidator,
400
+ contains: containsValidator,
401
+ maxContains: maxContainsValidator,
402
+ minContains: minContainsValidator,
403
+ unevaluatedItems: unevaluatedItemsValidator,
404
+ // object validators
405
+ maxProperties: maxPropertiesValidator,
406
+ minProperties: minPropertiesValidator,
407
+ required: requiredValidator,
408
+ additionalProperties: additionalPropertiesValidator,
409
+ patternProperties: patternPropertiesValidator,
410
+ properties: propertiesValidator,
411
+ dependencies: dependenciesValidator,
412
+ dependentSchemas: dependentSchemasValidator,
413
+ dependentRequired: dependentRequiredValidator,
414
+ propertyNames: propertyNamesValidator,
415
+ unevaluatedProperties: unevaluatedPropertiesValidator,
416
+ // combinator validators
417
+ allOf: allOfValidator,
418
+ anyOf: anyOfValidator,
419
+ oneOf: oneOfValidator,
420
+ not: notValidator,
421
+ if: ifValidator,
422
+ then: thenValidator,
423
+ else: elseValidator,
424
+ // misc
425
+ definitions: definitionsValidator,
686
426
  };
427
+ // ---------------------------------------------------------------------------
428
+ // recurseArray
429
+ // ---------------------------------------------------------------------------
687
430
  const recurseArray = function (report, schema, json) {
688
431
  // http://json-schema.org/latest/json-schema-validation.html#rfc.section.8.2
689
- let idx = json.length;
432
+ const schemaUri = typeof schema.$schema === 'string' ? schema.$schema : undefined;
433
+ const isDraft202012Schema = schemaUri === 'https://json-schema.org/draft/2020-12/schema' ||
434
+ (!schemaUri && this.options.version === 'draft2020-12');
435
+ const prefixItems = isDraft202012Schema && Array.isArray(schema.prefixItems) ? schema.prefixItems : undefined;
436
+ if (prefixItems) {
437
+ for (let idx = 0; idx < json.length; idx++) {
438
+ if (idx < prefixItems.length) {
439
+ report.path.push(idx);
440
+ validate.call(this, report, prefixItems[idx], json[idx]);
441
+ report.path.pop();
442
+ }
443
+ else if (schema.items !== undefined && !Array.isArray(schema.items)) {
444
+ report.path.push(idx);
445
+ report.schemaPath.push('items');
446
+ validate.call(this, report, schema.items, json[idx]);
447
+ report.schemaPath.pop();
448
+ report.path.pop();
449
+ }
450
+ }
451
+ return;
452
+ }
690
453
  // If "items" is an array, this situation, the schema depends on the index:
691
454
  // if the index is less than, or equal to, the size of "items",
692
455
  // the child instance must be valid against the corresponding schema in the "items" array;
693
456
  // otherwise, it must be valid against the schema defined by "additionalItems".
694
457
  if (Array.isArray(schema.items)) {
695
- while (idx--) {
458
+ for (let idx = 0; idx < json.length; idx++) {
696
459
  // equal to doesn't make sense here
697
460
  if (idx < schema.items.length) {
698
461
  report.path.push(idx);
@@ -712,7 +475,7 @@ const recurseArray = function (report, schema, json) {
712
475
  else if (typeof schema.items === 'object' || typeof schema.items === 'boolean') {
713
476
  // If items is a schema, then the child instance must be valid against this schema,
714
477
  // regardless of its index, and regardless of the value of "additionalItems".
715
- while (idx--) {
478
+ for (let idx = 0; idx < json.length; idx++) {
716
479
  report.path.push(idx);
717
480
  // Track schema path for array items validation
718
481
  report.schemaPath.push('items');
@@ -722,6 +485,9 @@ const recurseArray = function (report, schema, json) {
722
485
  }
723
486
  }
724
487
  };
488
+ // ---------------------------------------------------------------------------
489
+ // recurseObject
490
+ // ---------------------------------------------------------------------------
725
491
  const recurseObject = function (report, schema, json) {
726
492
  // http://json-schema.org/latest/json-schema-validation.html#rfc.section.8.3
727
493
  // If "additionalProperties" is absent, it is considered present with an empty schema as a value.
@@ -736,19 +502,16 @@ const recurseObject = function (report, schema, json) {
736
502
  const pp = schema.patternProperties ? Object.keys(schema.patternProperties) : [];
737
503
  // m - The property name of the child.
738
504
  const keys = Object.keys(json);
739
- let idx = keys.length;
740
- while (idx--) {
741
- const m = keys[idx], propertyValue = json[m];
505
+ for (const m of keys) {
506
+ const propertyValue = json[m];
742
507
  // s - The set of schemas for the child instance.
743
508
  const s = [];
744
509
  // 1. If set "p" contains value "m", then the corresponding schema in "properties" is added to "s".
745
- if (p.indexOf(m) !== -1) {
510
+ if (p.includes(m)) {
746
511
  s.push(schema.properties[m]);
747
512
  }
748
513
  // 2. For each regex in "pp", if it matches "m" successfully, the corresponding schema in "patternProperties" is added to "s".
749
- let idx2 = pp.length;
750
- while (idx2--) {
751
- const regexString = pp[idx2];
514
+ for (const regexString of pp) {
752
515
  const result = compileSchemaRegex(regexString);
753
516
  if (result.ok && result.value.test(m) === true) {
754
517
  s.push(schema.patternProperties[regexString]);
@@ -762,11 +525,10 @@ const recurseObject = function (report, schema, json) {
762
525
  // if s is empty in this stage, no additionalProperties are allowed
763
526
  // report.expect(s.length !== 0, 'E001', m);
764
527
  // Instance property value must pass all schemas from s
765
- idx2 = s.length;
766
- while (idx2--) {
528
+ for (const schema_s of s) {
767
529
  report.path.push(m);
768
530
  // Track schema path for properties validation
769
- if (p.indexOf(m) !== -1) {
531
+ if (p.includes(m)) {
770
532
  // This is a defined property
771
533
  report.schemaPath.push('properties');
772
534
  report.schemaPath.push(m);
@@ -775,15 +537,18 @@ const recurseObject = function (report, schema, json) {
775
537
  // This is additionalProperties or patternProperties
776
538
  report.schemaPath.push('additionalProperties');
777
539
  }
778
- validate.call(this, report, s[idx2], propertyValue);
540
+ validate.call(this, report, schema_s, propertyValue);
779
541
  report.path.pop();
780
542
  report.schemaPath.pop();
781
- if (p.indexOf(m) !== -1) {
543
+ if (p.includes(m)) {
782
544
  report.schemaPath.pop(); // pop the property name for defined properties
783
545
  }
784
546
  }
785
547
  }
786
548
  };
549
+ // ---------------------------------------------------------------------------
550
+ // validate — main entry point
551
+ // ---------------------------------------------------------------------------
787
552
  export function validate(report, schema, json) {
788
553
  report.commonErrorMessage = 'JSON_OBJECT_VALIDATION_FAILED';
789
554
  if (schema === true) {
@@ -809,50 +574,134 @@ export function validate(report, schema, json) {
809
574
  report.rootSchema = schema;
810
575
  isRoot = true;
811
576
  }
577
+ const recursiveAnchorStack = report.__$recursiveAnchorStack;
578
+ const dynamicScopeStack = report.__$dynamicScopeStack;
579
+ let pushedRecursiveAnchor = false;
580
+ let pushedDynamicScope = false;
581
+ const schemaId = getId(schema);
582
+ const schemaResourceRoot = schema.__$resourceRoot;
583
+ const dynamicScopeEntry = schemaResourceRoot || (isRoot || typeof schemaId === 'string' ? schema : undefined);
584
+ if (dynamicScopeEntry && dynamicScopeStack[dynamicScopeStack.length - 1] !== dynamicScopeEntry) {
585
+ dynamicScopeStack.push(dynamicScopeEntry);
586
+ pushedDynamicScope = true;
587
+ }
588
+ if (schema.$recursiveAnchor === true) {
589
+ recursiveAnchorStack.push(schema);
590
+ pushedRecursiveAnchor = true;
591
+ }
812
592
  // follow schema.$ref keys
813
593
  if (schema.$ref !== undefined) {
814
- // avoid infinite loop with maxRefs
815
- let maxRefs = 99;
816
- while (schema.$ref && maxRefs > 0) {
594
+ const applySiblingKeywordsWithRef = this.options.version === 'draft2019-09' || this.options.version === 'draft2020-12';
595
+ if (applySiblingKeywordsWithRef) {
817
596
  if (!schema.__$refResolved) {
818
597
  report.addError('REF_UNRESOLVED', [schema.$ref], undefined, schema);
819
- break;
820
598
  }
821
- else if (schema.__$refResolved === schema) {
822
- break;
599
+ else {
600
+ validate.call(this, report, schema.__$refResolved, json);
601
+ }
602
+ keys = keys.filter((key) => key !== '$ref');
603
+ }
604
+ else {
605
+ // avoid infinite loop with maxRefs
606
+ let maxRefs = 99;
607
+ while (schema.$ref && maxRefs > 0) {
608
+ if (!schema.__$refResolved) {
609
+ report.addError('REF_UNRESOLVED', [schema.$ref], undefined, schema);
610
+ break;
611
+ }
612
+ else if (schema.__$refResolved === schema) {
613
+ break;
614
+ }
615
+ else {
616
+ schema = schema.__$refResolved;
617
+ keys = Object.keys(schema);
618
+ }
619
+ maxRefs--;
620
+ }
621
+ if (maxRefs === 0) {
622
+ throw new Error('Circular dependency by $ref references!');
623
+ }
624
+ // Reset schema path for referenced schema - paths are relative to the referenced schema
625
+ report.schemaPath = [];
626
+ }
627
+ }
628
+ // follow schema.$recursiveRef keys
629
+ if (schema.$recursiveRef !== undefined) {
630
+ const applySiblingKeywordsWithRecursiveRef = this.options.version === 'draft2019-09' || this.options.version === 'draft2020-12';
631
+ if (applySiblingKeywordsWithRecursiveRef) {
632
+ const recursiveRefTarget = resolveRecursiveRef(schema, recursiveAnchorStack);
633
+ if (!recursiveRefTarget) {
634
+ report.addError('REF_UNRESOLVED', [schema.$recursiveRef], undefined, schema);
823
635
  }
824
636
  else {
825
- schema = schema.__$refResolved;
826
- keys = Object.keys(schema);
637
+ validate.call(this, report, recursiveRefTarget, json);
827
638
  }
828
- maxRefs--;
639
+ keys = keys.filter((key) => key !== '$recursiveRef');
829
640
  }
830
- if (maxRefs === 0) {
831
- throw new Error('Circular dependency by $ref references!');
641
+ }
642
+ // follow schema.$dynamicRef keys
643
+ if (schema.$dynamicRef !== undefined) {
644
+ const applySiblingKeywordsWithDynamicRef = this.options.version === 'draft2020-12';
645
+ if (applySiblingKeywordsWithDynamicRef) {
646
+ const dynamicRefTarget = resolveDynamicRef(schema, dynamicScopeStack);
647
+ if (typeof dynamicRefTarget === 'undefined') {
648
+ report.addError('REF_UNRESOLVED', [schema.$dynamicRef], undefined, schema);
649
+ }
650
+ else {
651
+ validate.call(this, report, dynamicRefTarget, json);
652
+ }
653
+ keys = keys.filter((key) => key !== '$dynamicRef');
832
654
  }
833
- // Reset schema path for referenced schema - paths are relative to the referenced schema
834
- report.schemaPath = [];
655
+ }
656
+ const validationVocabularyEnabled = isValidationVocabularyEnabled(schema, report, this.options.version);
657
+ if (!validationVocabularyEnabled) {
658
+ keys = keys.filter((key) => !VALIDATION_VOCAB_KEYWORDS.has(key));
835
659
  }
836
660
  // type checking first
837
- if (schema.type) {
661
+ if (validationVocabularyEnabled && schema.type) {
838
662
  keys.splice(keys.indexOf('type'), 1);
839
663
  report.schemaPath.push('type');
840
664
  JsonValidators.type.call(this, report, schema, json);
841
665
  report.schemaPath.pop();
842
666
  if (report.errors.length && this.options.breakOnFirstError) {
667
+ if (pushedRecursiveAnchor) {
668
+ recursiveAnchorStack.pop();
669
+ }
670
+ if (pushedDynamicScope) {
671
+ dynamicScopeStack.pop();
672
+ }
843
673
  return false;
844
674
  }
845
675
  }
846
676
  // now iterate all the keys in schema and execute validation methods
847
- let idx = keys.length;
848
- while (idx--) {
849
- if (JsonValidators[keys[idx]]) {
850
- JsonValidators[keys[idx]].call(this, report, schema, json);
677
+ // Defer unevaluatedItems/unevaluatedProperties to run after other validators,
678
+ // so combinator validation results are cached and available for annotation collection
679
+ const deferredUnevaluatedKeys = [];
680
+ for (const key of keys) {
681
+ if (key === 'unevaluatedItems' || key === 'unevaluatedProperties') {
682
+ deferredUnevaluatedKeys.push(key);
683
+ continue;
684
+ }
685
+ const validator = JsonValidators[key];
686
+ if (validator) {
687
+ validator.call(this, report, schema, json);
851
688
  if (report.errors.length && this.options.breakOnFirstError) {
852
689
  break;
853
690
  }
854
691
  }
855
692
  }
693
+ // Run unevaluated* validators after all others have cached their combinator results
694
+ if (deferredUnevaluatedKeys.length > 0 && !(report.errors.length > 0 && this.options.breakOnFirstError)) {
695
+ for (const key of deferredUnevaluatedKeys) {
696
+ const validator = JsonValidators[key];
697
+ if (validator) {
698
+ validator.call(this, report, schema, json);
699
+ if (report.errors.length && this.options.breakOnFirstError) {
700
+ break;
701
+ }
702
+ }
703
+ }
704
+ }
856
705
  if (report.errors.length === 0 || this.options.breakOnFirstError === false) {
857
706
  if (Array.isArray(json)) {
858
707
  recurseArray.call(this, report, schema, json);
@@ -864,6 +713,12 @@ export function validate(report, schema, json) {
864
713
  if (typeof this.options.customValidator === 'function') {
865
714
  this.options.customValidator.call(this, report, schema, json);
866
715
  }
716
+ if (pushedRecursiveAnchor) {
717
+ recursiveAnchorStack.pop();
718
+ }
719
+ if (pushedDynamicScope) {
720
+ dynamicScopeStack.pop();
721
+ }
867
722
  // we don't need the root pointer anymore
868
723
  if (isRoot) {
869
724
  report.rootSchema = undefined;