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
package/dist/errors.js CHANGED
@@ -12,6 +12,7 @@ export const Errors = {
12
12
  ARRAY_LENGTH_LONG: 'Array is too long ({0}), maximum {1}',
13
13
  ARRAY_UNIQUE: 'Array items are not unique (indexes {0} and {1})',
14
14
  ARRAY_ADDITIONAL_ITEMS: 'Additional items not allowed',
15
+ ARRAY_UNEVALUATED_ITEMS: 'Unevaluated items are not allowed',
15
16
  // Numeric errors
16
17
  MULTIPLE_OF: 'Value {0} is not a multiple of {1}',
17
18
  MINIMUM: 'Value {0} is less than minimum {1}',
@@ -23,6 +24,7 @@ export const Errors = {
23
24
  OBJECT_PROPERTIES_MAXIMUM: 'Too many properties defined ({0}), maximum {1}',
24
25
  OBJECT_MISSING_REQUIRED_PROPERTY: 'Missing required property: {0}',
25
26
  OBJECT_ADDITIONAL_PROPERTIES: 'Additional properties not allowed: {0}',
27
+ OBJECT_UNEVALUATED_PROPERTIES: 'Unevaluated properties are not allowed: {0}',
26
28
  OBJECT_DEPENDENCY_KEY: 'Dependency failed - key must exist: {0} (due to key: {1})',
27
29
  // String errors
28
30
  MIN_LENGTH: 'String is too short ({0} chars), minimum {1}',
@@ -52,6 +54,9 @@ export const Errors = {
52
54
  CONST: 'Value does not match const: {0}',
53
55
  CONTAINS: 'Array does not contain an item matching the schema',
54
56
  PROPERTY_NAMES: 'Property name {0} does not match the propertyNames schema',
57
+ // Draft-2019-09+ errors
58
+ COLLECT_EVALUATED_DEPTH_EXCEEDED: 'Schema nesting depth exceeded maximum ({0}) during unevaluated items/properties collection',
59
+ MAX_RECURSION_DEPTH_EXCEEDED: 'Maximum recursion depth ({0}) exceeded. If your schema or data is deeply nested and valid, increase the maxRecursionDepth option.',
55
60
  };
56
61
  export class ValidateError extends Error {
57
62
  name;
@@ -1,7 +1,10 @@
1
1
  import isEmailModule from 'validator/lib/isEmail.js';
2
2
  import isIPModule from 'validator/lib/isIP.js';
3
3
  import isURLModule from 'validator/lib/isURL.js';
4
+ import { isValidRfc3339Date } from './utils/date.js';
5
+ import { isValidHostname, isValidIdnHostname } from './utils/hostname.js';
4
6
  import { sortedKeys } from './utils/json.js';
7
+ import { parseRfc3339Time } from './utils/time.js';
5
8
  const dateValidator = (date) => {
6
9
  if (typeof date !== 'string') {
7
10
  return true;
@@ -11,13 +14,10 @@ const dateValidator = (date) => {
11
14
  if (matches === null) {
12
15
  return false;
13
16
  }
14
- // var year = matches[1];
15
- // var month = matches[2];
16
- // var day = matches[3];
17
- if (matches[2] < '01' || matches[2] > '12' || matches[3] < '01' || matches[3] > '31') {
18
- return false;
19
- }
20
- return true;
17
+ const year = parseInt(matches[1], 10);
18
+ const month = parseInt(matches[2], 10);
19
+ const day = parseInt(matches[3], 10);
20
+ return isValidRfc3339Date(year, month, day);
21
21
  };
22
22
  const dateTimeValidator = (dateTime) => {
23
23
  if (typeof dateTime !== 'string') {
@@ -38,111 +38,34 @@ const dateTimeValidator = (dateTime) => {
38
38
  const year = parseInt(dateMatches[1], 10);
39
39
  const month = parseInt(dateMatches[2], 10);
40
40
  const day = parseInt(dateMatches[3], 10);
41
- if (month < 1 || month > 12 || day < 1 || day > 31) {
42
- return false;
43
- }
44
- // Check if date is valid
45
- const date = new Date(year, month - 1, day);
46
- if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
47
- return false;
48
- }
49
- // Check time
50
- const timeMatches = /^([0-9]{2}):([0-9]{2}):([0-9]{2})(.[0-9]+)?(z|([+-][0-9]{2}:[0-9]{2}))$/.exec(timePart);
51
- if (timeMatches === null) {
41
+ if (!isValidRfc3339Date(year, month, day)) {
52
42
  return false;
53
43
  }
54
- const hour = parseInt(timeMatches[1], 10);
55
- const minute = parseInt(timeMatches[2], 10);
56
- const second = parseInt(timeMatches[3], 10);
57
- if (hour > 23 || minute > 59 || second > 60) {
58
- return false;
59
- }
60
- // Check offset
61
- let utcHour = hour;
62
- if (timeMatches[5] !== 'z') {
63
- const offset = timeMatches[5];
64
- const offsetMatches = /^([+-])([0-9]{2}):([0-9]{2})$/.exec(offset);
65
- if (offsetMatches === null) {
66
- return false;
67
- }
68
- const offsetSign = offsetMatches[1];
69
- const offsetHour = parseInt(offsetMatches[2], 10);
70
- const offsetMinute = parseInt(offsetMatches[3], 10);
71
- if (offsetHour > 23 || offsetMinute > 59) {
72
- return false;
73
- }
74
- if (offsetSign === '+') {
75
- utcHour = hour - offsetHour;
76
- }
77
- else {
78
- utcHour = hour + offsetHour;
79
- }
80
- utcHour = ((utcHour % 24) + 24) % 24;
81
- }
82
- // Leap second only at 23:59:60 UTC
83
- if (second === 60) {
84
- if (utcHour !== 23 || minute !== 59) {
85
- return false;
86
- }
87
- }
88
- return true;
44
+ return parseRfc3339Time(timePart) !== null;
89
45
  };
90
46
  const emailValidator = (email) => {
91
47
  if (typeof email !== 'string') {
92
48
  return true;
93
49
  }
94
- return isEmailModule.default(email, { require_tld: true });
50
+ if (isEmailModule.default(email, { require_tld: true, allow_ip_domain: true })) {
51
+ return true;
52
+ }
53
+ const ipv6Literal = /^(.+)@\[IPv6:([^\]]+)\]$/i.exec(email);
54
+ if (!ipv6Literal) {
55
+ return false;
56
+ }
57
+ const localPart = ipv6Literal[1];
58
+ const addressPart = ipv6Literal[2];
59
+ if (!isIPModule.default(addressPart, 6)) {
60
+ return false;
61
+ }
62
+ return isEmailModule.default(`${localPart}@example.com`, { require_tld: true });
95
63
  };
96
64
  const hostnameValidator = (hostname) => {
97
65
  if (typeof hostname !== 'string') {
98
66
  return true;
99
67
  }
100
- /*
101
- http://json-schema.org/latest/json-schema-validation.html#anchor114
102
- A string instance is valid against this attribute if it is a valid
103
- representation for an Internet host name, as defined by RFC 1034, section 3.1 [RFC1034].
104
-
105
- http://tools.ietf.org/html/rfc1034#section-3.5
106
-
107
- <digit> ::= any one of the ten digits 0 through 9
108
- var digit = /[0-9]/;
109
-
110
- <letter> ::= any one of the 52 alphabetic characters A through Z in upper case and a through z in lower case
111
- var letter = /[a-zA-Z]/;
112
-
113
- <let-dig> ::= <letter> | <digit>
114
- var letDig = /[0-9a-zA-Z]/;
115
-
116
- <let-dig-hyp> ::= <let-dig> | "-"
117
- var letDigHyp = /[-0-9a-zA-Z]/;
118
-
119
- <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
120
- var ldhStr = /[-0-9a-zA-Z]+/;
121
-
122
- <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
123
- var label = /[a-zA-Z](([-0-9a-zA-Z]+)?[0-9a-zA-Z])?/;
124
-
125
- <subdomain> ::= <label> | <subdomain> "." <label>
126
- var subdomain = /^[a-zA-Z](([-0-9a-zA-Z]+)?[0-9a-zA-Z])?(\.[a-zA-Z](([-0-9a-zA-Z]+)?[0-9a-zA-Z])?)*$/;
127
-
128
- <domain> ::= <subdomain> | " "
129
- var domain = null;
130
- */
131
- const valid = /^[a-zA-Z](([-0-9a-zA-Z]+)?[0-9a-zA-Z])?(\.[a-zA-Z](([-0-9a-zA-Z]+)?[0-9a-zA-Z])?)*$/.test(hostname);
132
- if (valid) {
133
- // the sum of all label octets and label lengths is limited to 255.
134
- if (hostname.length > 255) {
135
- return false;
136
- }
137
- // Each node has a label, which is zero to 63 octets in length
138
- const labels = hostname.split('.');
139
- for (let i = 0; i < labels.length; i++) {
140
- if (labels[i].length > 63) {
141
- return false;
142
- }
143
- }
144
- }
145
- return valid;
68
+ return isValidHostname(hostname);
146
69
  };
147
70
  const ipv4Validator = (ipv4) => {
148
71
  if (typeof ipv4 !== 'string') {
@@ -161,7 +84,21 @@ const ipv6Validator = (ipv6) => {
161
84
  };
162
85
  const regexValidator = (input) => {
163
86
  if (typeof input !== 'string') {
164
- return false;
87
+ return true;
88
+ }
89
+ const invalidEscapes = new Set(['a']);
90
+ for (let idx = 0; idx < input.length; idx++) {
91
+ if (input[idx] !== '\\') {
92
+ continue;
93
+ }
94
+ idx++;
95
+ if (idx >= input.length) {
96
+ return false;
97
+ }
98
+ const escaped = input[idx];
99
+ if (invalidEscapes.has(escaped)) {
100
+ return false;
101
+ }
165
102
  }
166
103
  try {
167
104
  RegExp(input);
@@ -171,6 +108,55 @@ const regexValidator = (input) => {
171
108
  return false;
172
109
  }
173
110
  };
111
+ const durationValidator = (input) => {
112
+ if (typeof input !== 'string') {
113
+ return true;
114
+ }
115
+ // eslint-disable-next-line no-control-regex
116
+ if (!/^P[\x00-\x7F]*$/.test(input)) {
117
+ return false;
118
+ }
119
+ if (!input.startsWith('P')) {
120
+ return false;
121
+ }
122
+ const body = input.slice(1);
123
+ if (body.length === 0) {
124
+ return false;
125
+ }
126
+ if (body.includes('W')) {
127
+ return /^\d+W$/.test(body);
128
+ }
129
+ const parts = body.split('T');
130
+ if (parts.length > 2) {
131
+ return false;
132
+ }
133
+ const datePart = parts[0];
134
+ const timePart = parts.length === 2 ? parts[1] : undefined;
135
+ if (!/^(?:\d+Y)?(?:\d+M)?(?:\d+D)?$/.test(datePart)) {
136
+ return false;
137
+ }
138
+ const hasDateComponent = /\d+[YMD]/.test(datePart);
139
+ let hasTimeComponent = false;
140
+ if (timePart !== undefined) {
141
+ if (timePart.length === 0) {
142
+ return false;
143
+ }
144
+ if (!/^(?:\d+H)?(?:\d+M)?(?:\d+S)?$/.test(timePart)) {
145
+ return false;
146
+ }
147
+ hasTimeComponent = /\d+[HMS]/.test(timePart);
148
+ if (!hasTimeComponent) {
149
+ return false;
150
+ }
151
+ }
152
+ return hasDateComponent || hasTimeComponent;
153
+ };
154
+ const uuidValidator = (input) => {
155
+ if (typeof input !== 'string') {
156
+ return true;
157
+ }
158
+ return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(input);
159
+ };
174
160
  const strictUriValidator = (uri) => typeof uri !== 'string' || isURLModule.default(uri);
175
161
  const uriValidator = function (uri) {
176
162
  if (typeof uri !== 'string')
@@ -235,14 +221,14 @@ const jsonPointerValidator = (pointer) => {
235
221
  const relativeJsonPointerValidator = (pointer) => {
236
222
  if (typeof pointer !== 'string')
237
223
  return true;
238
- // Relative JSON Pointer: number#path or empty
239
- return /^\d+(#.*)?$/.test(pointer) || pointer === '';
224
+ // Relative JSON Pointer: non-negative integer prefix (no leading zeros unless zero),
225
+ // followed by either '#', a JSON Pointer, or nothing.
226
+ return /^(?:0|[1-9]\d*)(?:#|(?:\/(?:[^~]|~0|~1)*)+)?$/.test(pointer);
240
227
  };
241
228
  const timeValidator = (time) => {
242
229
  if (typeof time !== 'string')
243
230
  return true;
244
- // time: hh:mm:ss[.fraction]
245
- return /^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)(\.\d+)?$/.test(time);
231
+ return parseRfc3339Time(time) !== null;
246
232
  };
247
233
  const idnEmailValidator = (email) => {
248
234
  if (typeof email !== 'string')
@@ -250,6 +236,30 @@ const idnEmailValidator = (email) => {
250
236
  // Simple email check, allowing international chars
251
237
  return /^[^\s@]+@[^\s@]+$/.test(email);
252
238
  };
239
+ const idnHostnameValidator = (hostname) => {
240
+ if (typeof hostname !== 'string')
241
+ return true;
242
+ return isValidIdnHostname(hostname);
243
+ };
244
+ const iriValidator = (iri) => {
245
+ if (typeof iri !== 'string')
246
+ return true;
247
+ if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:[^"\\<>^{}^`| ]*$/u.test(iri)) {
248
+ return false;
249
+ }
250
+ try {
251
+ new URL(iri);
252
+ return true;
253
+ }
254
+ catch (_e) {
255
+ return false;
256
+ }
257
+ };
258
+ const iriReferenceValidator = (iriReference) => {
259
+ if (typeof iriReference !== 'string')
260
+ return true;
261
+ return /^([a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^{}^`| ]*$/u.test(iriReference);
262
+ };
253
263
  const inbuiltValidators = {
254
264
  date: dateValidator,
255
265
  'date-time': dateTimeValidator,
@@ -267,6 +277,11 @@ const inbuiltValidators = {
267
277
  'relative-json-pointer': relativeJsonPointerValidator,
268
278
  time: timeValidator,
269
279
  'idn-email': idnEmailValidator,
280
+ 'idn-hostname': idnHostnameValidator,
281
+ iri: iriValidator,
282
+ 'iri-reference': iriReferenceValidator,
283
+ duration: durationValidator,
284
+ uuid: uuidValidator,
270
285
  };
271
286
  const customValidators = {};
272
287
  export function getFormatValidators(options) {
@@ -293,8 +308,17 @@ export function getSupportedFormats(customFormats) {
293
308
  return keys.filter((key) => merged[key] != null);
294
309
  }
295
310
  export function isFormatSupported(name, customFormats) {
296
- const supported = getSupportedFormats(customFormats);
297
- return supported.includes(name);
311
+ if (customFormats) {
312
+ const custom = customFormats[name];
313
+ // Explicitly null means unregistered at instance level
314
+ if (custom === null)
315
+ return false;
316
+ if (custom != null)
317
+ return true;
318
+ }
319
+ if (name in customValidators)
320
+ return customValidators[name] != null;
321
+ return name in inbuiltValidators;
298
322
  }
299
323
  export function getRegisteredFormats() {
300
324
  return sortedKeys(customValidators);
@@ -1,5 +1,8 @@
1
- export const CURRENT_DEFAULT_SCHEMA_VERSION = 'draft-06';
1
+ export const CURRENT_DEFAULT_SCHEMA_VERSION = 'draft2020-12';
2
2
  export const VERSION_SCHEMA_URL_MAPPING = {
3
3
  'draft-04': 'http://json-schema.org/draft-04/schema#',
4
4
  'draft-06': 'http://json-schema.org/draft-06/schema#',
5
+ 'draft-07': 'http://json-schema.org/draft-07/schema#',
6
+ 'draft2019-09': 'https://json-schema.org/draft/2019-09/schema',
7
+ 'draft2020-12': 'https://json-schema.org/draft/2020-12/schema',
5
8
  };
@@ -1,14 +1,23 @@
1
+ import { getRemotePath, isAbsoluteUri } from './utils/uri.js';
2
+ /**
3
+ * Keywords whose values are not JSON Schema sub-schemas and must not be
4
+ * traversed during schema walking (id collection, reference collection, etc.).
5
+ */
6
+ export const NON_SCHEMA_KEYWORDS = ['enum', 'const', 'default', 'examples'];
7
+ /** Returns true if the key is an internal z-schema property (prefixed with `__$`). */
8
+ export const isInternalKey = (key) => key.startsWith('__$');
1
9
  import { isObject } from './utils/what-is.js';
10
+ import { DEFAULT_MAX_RECURSION_DEPTH } from './z-schema-options.js';
2
11
  export const getId = (schema) => {
3
- if (schema.$id) {
4
- return schema.$id;
5
- }
6
- if (schema.id) {
12
+ // Draft-04 uses `id` exclusively — never return `$id` for a draft-04 schema
13
+ if (typeof schema.$schema === 'string' && schema.$schema.includes('draft-04')) {
7
14
  return schema.id;
8
15
  }
9
- return undefined;
16
+ // Draft-06+ uses `$id`; fall back to `id` for backward compatibility
17
+ // with schemas that haven't migrated to `$id` yet.
18
+ return schema.$id ?? schema.id;
10
19
  };
11
- export const findId = (schema, id) => {
20
+ export const findId = (schema, id, targetBaseUri, currentBaseUri, maxDepth = DEFAULT_MAX_RECURSION_DEPTH, _depth = 0) => {
12
21
  // process only arrays and objects
13
22
  if (typeof schema !== 'object' || schema === null) {
14
23
  return;
@@ -17,17 +26,39 @@ export const findId = (schema, id) => {
17
26
  if (!id) {
18
27
  return schema;
19
28
  }
29
+ if (_depth >= maxDepth) {
30
+ throw new Error(`Maximum recursion depth (${maxDepth}) exceeded in findId. ` +
31
+ 'If your schema is deeply nested and valid, increase the maxRecursionDepth option.');
32
+ }
33
+ const baseUri = currentBaseUri ?? targetBaseUri;
20
34
  const schemaId = getId(schema);
35
+ let nextBaseUri = baseUri;
21
36
  if (schemaId) {
22
- if (schemaId === id || (schemaId[0] === '#' && schemaId.substring(1) === id)) {
37
+ if (isAbsoluteUri(schemaId)) {
38
+ nextBaseUri = getRemotePath(schemaId);
39
+ }
40
+ else if (baseUri && isAbsoluteUri(baseUri)) {
41
+ try {
42
+ nextBaseUri = getRemotePath(new URL(schemaId, baseUri).toString());
43
+ }
44
+ catch {
45
+ // keep existing scope when URL resolution fails
46
+ }
47
+ }
48
+ }
49
+ const inTargetBase = !targetBaseUri || nextBaseUri === targetBaseUri;
50
+ if (inTargetBase) {
51
+ if (schemaId && (schemaId === id || (schemaId[0] === '#' && schemaId.substring(1) === id))) {
52
+ return schema;
53
+ }
54
+ if (schema.$anchor === id || schema.$dynamicAnchor === id) {
23
55
  return schema;
24
56
  }
25
57
  }
26
- let idx, result;
58
+ let result;
27
59
  if (Array.isArray(schema)) {
28
- idx = schema.length;
29
- while (idx--) {
30
- result = findId(schema[idx], id);
60
+ for (let i = 0; i < schema.length; i++) {
61
+ result = findId(schema[i], id, targetBaseUri, nextBaseUri, maxDepth, _depth + 1);
31
62
  if (result) {
32
63
  return result;
33
64
  }
@@ -35,13 +66,16 @@ export const findId = (schema, id) => {
35
66
  }
36
67
  if (isObject(schema)) {
37
68
  const keys = Object.keys(schema);
38
- idx = keys.length;
39
- while (idx--) {
40
- const k = keys[idx];
41
- if (k.indexOf('__$') === 0) {
69
+ // Reverse iteration: when sibling sub-schemas share the same $dynamicAnchor
70
+ // name, the LAST sibling (by key order) must win. This is required for
71
+ // $dynamicRef correctness — e.g. the optional test "$dynamicRef skips over
72
+ // intermediate resources - pointer reference across resource boundary".
73
+ for (let i = keys.length - 1; i >= 0; i--) {
74
+ const k = keys[i];
75
+ if (isInternalKey(k) || NON_SCHEMA_KEYWORDS.includes(k)) {
42
76
  continue;
43
77
  }
44
- result = findId(schema[k], id);
78
+ result = findId(schema[k], id, targetBaseUri, nextBaseUri, maxDepth, _depth + 1);
45
79
  if (result) {
46
80
  return result;
47
81
  }