z-schema 12.2.0 → 12.4.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 (111) hide show
  1. package/README.md +2 -2
  2. package/bin/z-schema +1 -1
  3. package/cjs/{index.js → index.cjs} +838 -707
  4. package/cjs/{index.d.ts → index.d.cts} +47 -26
  5. package/dist/{errors.d.mts → errors.d.ts} +2 -2
  6. package/dist/{errors.mjs → errors.js} +1 -2
  7. package/dist/{format-validators.mjs → format-validators.js} +60 -43
  8. package/dist/{index.d.mts → index.d.ts} +9 -9
  9. package/dist/{index.mjs → index.js} +3 -3
  10. package/dist/{json-schema-versions.d.mts → json-schema-versions.d.ts} +34 -3
  11. package/dist/{json-schema.d.mts → json-schema.d.ts} +7 -7
  12. package/dist/{json-schema.mjs → json-schema.js} +6 -5
  13. package/dist/{json-validation.mjs → json-validation.js} +144 -127
  14. package/dist/{report.d.mts → report.d.ts} +8 -9
  15. package/dist/{report.mjs → report.js} +28 -31
  16. package/dist/{schema-cache.d.mts → schema-cache.d.ts} +5 -5
  17. package/dist/{schema-cache.mjs → schema-cache.js} +10 -11
  18. package/dist/{schema-compiler.d.mts → schema-compiler.d.ts} +4 -4
  19. package/dist/{schema-compiler.mjs → schema-compiler.js} +101 -79
  20. package/dist/{schema-validator.d.mts → schema-validator.d.ts} +5 -5
  21. package/dist/{schema-validator.mjs → schema-validator.js} +138 -166
  22. package/dist/utils/{array.mjs → array.js} +4 -3
  23. package/dist/utils/{base64.mjs → base64.js} +3 -2
  24. package/dist/utils/{clone.mjs → clone.js} +18 -20
  25. package/dist/utils/hostname.js +176 -0
  26. package/dist/utils/{json.mjs → json.js} +13 -8
  27. package/dist/utils/{schema-regex.mjs → schema-regex.js} +5 -5
  28. package/dist/utils/{time.mjs → time.js} +8 -8
  29. package/dist/utils/unicode.js +22 -0
  30. package/dist/utils/{what-is.mjs → what-is.js} +1 -2
  31. package/dist/validation/{array.mjs → array.js} +18 -20
  32. package/dist/validation/{combinators.mjs → combinators.js} +16 -16
  33. package/dist/validation/{numeric.mjs → numeric.js} +11 -11
  34. package/dist/validation/{object.mjs → object.js} +35 -34
  35. package/dist/validation/{ref.mjs → ref.js} +5 -5
  36. package/dist/validation/{shared.mjs → shared.js} +15 -12
  37. package/dist/validation/{string.mjs → string.js} +35 -35
  38. package/dist/validation/type.js +34 -0
  39. package/dist/{z-schema-base.d.mts → z-schema-base.d.ts} +11 -12
  40. package/dist/{z-schema-base.mjs → z-schema-base.js} +45 -40
  41. package/dist/{z-schema-options.d.mts → z-schema-options.d.ts} +3 -3
  42. package/dist/{z-schema-options.mjs → z-schema-options.js} +5 -5
  43. package/dist/{z-schema-reader.d.mts → z-schema-reader.d.ts} +1 -1
  44. package/dist/{z-schema-versions.mjs → z-schema-versions.js} +21 -21
  45. package/dist/{z-schema.d.mts → z-schema.d.ts} +5 -13
  46. package/dist/{z-schema.mjs → z-schema.js} +38 -48
  47. package/package.json +24 -25
  48. package/src/errors.ts +1 -2
  49. package/src/format-validators.ts +173 -67
  50. package/src/json-schema-versions.ts +56 -2
  51. package/src/json-schema.ts +10 -9
  52. package/src/json-validation.ts +189 -146
  53. package/src/report.ts +37 -49
  54. package/src/schema-cache.ts +13 -13
  55. package/src/schema-compiler.ts +170 -117
  56. package/src/schema-validator.ts +239 -238
  57. package/src/utils/array.ts +9 -6
  58. package/src/utils/base64.ts +13 -2
  59. package/src/utils/clone.ts +28 -30
  60. package/src/utils/date.ts +6 -3
  61. package/src/utils/hostname.ts +257 -31
  62. package/src/utils/json.ts +16 -9
  63. package/src/utils/properties.ts +2 -2
  64. package/src/utils/schema-regex.ts +4 -4
  65. package/src/utils/time.ts +8 -7
  66. package/src/utils/unicode.ts +12 -5
  67. package/src/utils/what-is.ts +1 -5
  68. package/src/validation/array.ts +24 -22
  69. package/src/validation/combinators.ts +14 -14
  70. package/src/validation/numeric.ts +14 -28
  71. package/src/validation/object.ts +32 -36
  72. package/src/validation/ref.ts +5 -6
  73. package/src/validation/shared.ts +22 -21
  74. package/src/validation/string.ts +36 -42
  75. package/src/validation/type.ts +17 -17
  76. package/src/z-schema-base.ts +49 -38
  77. package/src/z-schema-options.ts +4 -3
  78. package/src/z-schema.ts +35 -45
  79. package/umd/ZSchema.js +853 -715
  80. package/umd/ZSchema.min.js +2 -2
  81. package/umd/package.json +3 -0
  82. package/dist/utils/hostname.mjs +0 -75
  83. package/dist/utils/unicode.mjs +0 -12
  84. package/dist/validation/type.mjs +0 -32
  85. /package/dist/{format-validators.d.mts → format-validators.d.ts} +0 -0
  86. /package/dist/{json-schema-versions.mjs → json-schema-versions.js} +0 -0
  87. /package/dist/schemas/{draft-04-schema.mjs → draft-04-schema.js} +0 -0
  88. /package/dist/schemas/{draft-06-schema.mjs → draft-06-schema.js} +0 -0
  89. /package/dist/schemas/{draft-07-schema.mjs → draft-07-schema.js} +0 -0
  90. /package/dist/schemas/{draft-2019-09-meta-applicator.mjs → draft-2019-09-meta-applicator.js} +0 -0
  91. /package/dist/schemas/{draft-2019-09-meta-content.mjs → draft-2019-09-meta-content.js} +0 -0
  92. /package/dist/schemas/{draft-2019-09-meta-core.mjs → draft-2019-09-meta-core.js} +0 -0
  93. /package/dist/schemas/{draft-2019-09-meta-format.mjs → draft-2019-09-meta-format.js} +0 -0
  94. /package/dist/schemas/{draft-2019-09-meta-meta-data.mjs → draft-2019-09-meta-meta-data.js} +0 -0
  95. /package/dist/schemas/{draft-2019-09-meta-validation.mjs → draft-2019-09-meta-validation.js} +0 -0
  96. /package/dist/schemas/{draft-2019-09-schema.mjs → draft-2019-09-schema.js} +0 -0
  97. /package/dist/schemas/{draft-2020-12-meta-applicator.mjs → draft-2020-12-meta-applicator.js} +0 -0
  98. /package/dist/schemas/{draft-2020-12-meta-content.mjs → draft-2020-12-meta-content.js} +0 -0
  99. /package/dist/schemas/{draft-2020-12-meta-core.mjs → draft-2020-12-meta-core.js} +0 -0
  100. /package/dist/schemas/{draft-2020-12-meta-format-annotation.mjs → draft-2020-12-meta-format-annotation.js} +0 -0
  101. /package/dist/schemas/{draft-2020-12-meta-format-assertion.mjs → draft-2020-12-meta-format-assertion.js} +0 -0
  102. /package/dist/schemas/{draft-2020-12-meta-meta-data.mjs → draft-2020-12-meta-meta-data.js} +0 -0
  103. /package/dist/schemas/{draft-2020-12-meta-unevaluated.mjs → draft-2020-12-meta-unevaluated.js} +0 -0
  104. /package/dist/schemas/{draft-2020-12-meta-validation.mjs → draft-2020-12-meta-validation.js} +0 -0
  105. /package/dist/schemas/{draft-2020-12-schema.mjs → draft-2020-12-schema.js} +0 -0
  106. /package/dist/utils/{constants.mjs → constants.js} +0 -0
  107. /package/dist/utils/{date.mjs → date.js} +0 -0
  108. /package/dist/utils/{properties.mjs → properties.js} +0 -0
  109. /package/dist/utils/{symbols.mjs → symbols.js} +0 -0
  110. /package/dist/utils/{uri.mjs → uri.js} +0 -0
  111. /package/dist/{z-schema-reader.mjs → z-schema-reader.js} +0 -0
@@ -14,13 +14,13 @@ const dateValidator: FormatValidatorFn = (date: unknown) => {
14
14
  return true;
15
15
  }
16
16
  // full-date from http://tools.ietf.org/html/rfc3339#section-5.6
17
- const matches = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(date);
17
+ const matches = /^(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})$/.exec(date);
18
18
  if (matches === null) {
19
19
  return false;
20
20
  }
21
- const year = parseInt(matches[1], 10);
22
- const month = parseInt(matches[2], 10);
23
- const day = parseInt(matches[3], 10);
21
+ const year = Number.parseInt(matches[1], 10);
22
+ const month = Number.parseInt(matches[2], 10);
23
+ const day = Number.parseInt(matches[3], 10);
24
24
  return isValidRfc3339Date(year, month, day);
25
25
  };
26
26
 
@@ -29,20 +29,23 @@ const dateTimeValidator: FormatValidatorFn = (dateTime: unknown) => {
29
29
  return true;
30
30
  }
31
31
  // date-time from http://tools.ietf.org/html/rfc3339#section-5.6
32
- const s = dateTime.toLowerCase().split('t');
33
- if (s.length !== 2) {
32
+ let tIdx = dateTime.indexOf('T');
33
+ if (tIdx === -1) {
34
+ tIdx = dateTime.indexOf('t');
35
+ }
36
+ if (tIdx === -1) {
34
37
  return false;
35
38
  }
36
- const datePart = s[0];
37
- const timePart = s[1];
39
+ const datePart = dateTime.slice(0, tIdx);
40
+ const timePart = dateTime.slice(tIdx + 1);
38
41
  // Check date
39
- const dateMatches = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(datePart);
42
+ const dateMatches = /^(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})$/.exec(datePart);
40
43
  if (dateMatches === null) {
41
44
  return false;
42
45
  }
43
- const year = parseInt(dateMatches[1], 10);
44
- const month = parseInt(dateMatches[2], 10);
45
- const day = parseInt(dateMatches[3], 10);
46
+ const year = Number.parseInt(dateMatches[1], 10);
47
+ const month = Number.parseInt(dateMatches[2], 10);
48
+ const day = Number.parseInt(dateMatches[3], 10);
46
49
  if (!isValidRfc3339Date(year, month, day)) {
47
50
  return false;
48
51
  }
@@ -58,7 +61,7 @@ const emailValidator: FormatValidatorFn = (email: unknown) => {
58
61
  return true;
59
62
  }
60
63
 
61
- const ipv6Literal = /^(.+)@\[IPv6:([^\]]+)\]$/i.exec(email);
64
+ const ipv6Literal = /^(?<localPart>.+)@\[IPv6:(?<address>[^\]]+)\]$/i.exec(email);
62
65
  if (!ipv6Literal) {
63
66
  return false;
64
67
  }
@@ -96,12 +99,13 @@ const ipv6Validator: FormatValidatorFn = (ipv6: unknown) => {
96
99
  return isIPModule.default(ipv6, 6);
97
100
  };
98
101
 
102
+ const INVALID_REGEX_ESCAPES = new Set(['a']);
103
+
99
104
  const regexValidator: FormatValidatorFn = (input: unknown) => {
100
105
  if (typeof input !== 'string') {
101
106
  return true;
102
107
  }
103
108
 
104
- const invalidEscapes = new Set(['a']);
105
109
  for (let idx = 0; idx < input.length; idx++) {
106
110
  if (input[idx] !== '\\') {
107
111
  continue;
@@ -113,15 +117,19 @@ const regexValidator: FormatValidatorFn = (input: unknown) => {
113
117
  }
114
118
 
115
119
  const escaped = input[idx];
116
- if (invalidEscapes.has(escaped)) {
120
+ if (INVALID_REGEX_ESCAPES.has(escaped)) {
117
121
  return false;
118
122
  }
119
123
  }
120
124
 
121
125
  try {
122
- RegExp(input);
126
+ // Constructed purely to detect an invalid pattern (throws SyntaxError). The
127
+ // result is intentionally unused; `new` is required by new-for-builtins and
128
+ // `void`/call-form trip no-void/new-for-builtins, so no-new is disabled here.
129
+ // oxlint-disable-next-line no-new
130
+ new RegExp(input);
123
131
  return true;
124
- } catch (_e) {
132
+ } catch {
125
133
  return false;
126
134
  }
127
135
  };
@@ -132,7 +140,7 @@ const durationValidator: FormatValidatorFn = (input: unknown) => {
132
140
  }
133
141
 
134
142
  // eslint-disable-next-line no-control-regex
135
- if (!/^P[\x00-\x7F]*$/.test(input)) {
143
+ if (!/^P[\u0000-\u007F]*$/.test(input)) {
136
144
  return false;
137
145
  }
138
146
 
@@ -163,7 +171,7 @@ const durationValidator: FormatValidatorFn = (input: unknown) => {
163
171
  return false;
164
172
  }
165
173
 
166
- const hasDateComponent = /\d+[YMD]/.test(datePart);
174
+ const hasDateComponent = datePart.length > 0;
167
175
  let hasTimeComponent = false;
168
176
 
169
177
  if (timePart !== undefined) {
@@ -193,43 +201,55 @@ const uuidValidator: FormatValidatorFn = (input: unknown) => {
193
201
 
194
202
  const strictUriValidator: FormatValidatorFn = (uri: unknown) => typeof uri !== 'string' || isURLModule.default(uri);
195
203
 
204
+ const isHexChar = (c: number) => (c >= 48 && c <= 57) || (c >= 65 && c <= 70) || (c >= 97 && c <= 102);
205
+
196
206
  const hasValidPercentEncoding = (str: string): boolean => {
197
207
  for (let i = 0; i < str.length; i++) {
198
- if (str[i] === '%') {
199
- if (i + 2 >= str.length || !/[0-9a-fA-F]/.test(str[i + 1]) || !/[0-9a-fA-F]/.test(str[i + 2])) {
200
- return false;
201
- }
208
+ if (
209
+ str[i] === '%' &&
210
+ (i + 2 >= str.length || !isHexChar(str.charCodeAt(i + 1)) || !isHexChar(str.charCodeAt(i + 2)))
211
+ ) {
212
+ return false;
202
213
  }
203
214
  }
204
215
  return true;
205
216
  };
206
217
 
207
- const uriValidator: FormatValidatorFn = function (uri: unknown) {
208
- if (typeof uri !== 'string') return true;
218
+ const uriValidator: FormatValidatorFn = (uri: unknown) => {
219
+ if (typeof uri !== 'string') {
220
+ return true;
221
+ }
209
222
  // eslint-disable-next-line no-control-regex
210
- if (/[^\x00-\x7F]/.test(uri)) return false;
211
- if (!hasValidPercentEncoding(uri)) return false;
212
- const match = uri.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/?#]*)/);
223
+ if (/[^\u0000-\u007F]/.test(uri)) {
224
+ return false;
225
+ }
226
+ if (!hasValidPercentEncoding(uri)) {
227
+ return false;
228
+ }
229
+ const match = /^(?<scheme>[a-zA-Z][a-zA-Z0-9+.-]*):\/\/(?<authority>[^/?#]*)/.exec(uri);
213
230
  if (match) {
214
231
  const authority = match[2];
215
232
  const atIndex = authority.indexOf('@');
216
233
  if (atIndex > 0) {
217
- const userinfo = authority.substring(0, atIndex);
234
+ // userinfo is a string; prefer-set-has misfires here — two String#includes
235
+ // calls are faster than building a Set or a regex for this membership check.
236
+ // oxlint-disable-next-line unicorn/prefer-set-has
237
+ const userinfo = authority.slice(0, atIndex);
218
238
  if (userinfo.includes('[') || userinfo.includes(']')) {
219
239
  return false;
220
240
  }
221
241
  }
222
242
  // Validate port: must be numeric
223
- let hostPort = atIndex >= 0 ? authority.substring(atIndex + 1) : authority;
243
+ let hostPort = atIndex === -1 ? authority : authority.slice(atIndex + 1);
224
244
  if (hostPort.startsWith('[')) {
225
245
  const bracketEnd = hostPort.indexOf(']');
226
- if (bracketEnd >= 0) {
227
- hostPort = hostPort.substring(bracketEnd + 1);
246
+ if (bracketEnd !== -1) {
247
+ hostPort = hostPort.slice(bracketEnd + 1);
228
248
  }
229
249
  }
230
250
  const colonIndex = hostPort.lastIndexOf(':');
231
- if (colonIndex >= 0) {
232
- const port = hostPort.substring(colonIndex + 1);
251
+ if (colonIndex !== -1) {
252
+ const port = hostPort.slice(colonIndex + 1);
233
253
  if (port.length > 0 && !/^\d+$/.test(port)) {
234
254
  return false;
235
255
  }
@@ -239,17 +259,23 @@ const uriValidator: FormatValidatorFn = function (uri: unknown) {
239
259
  };
240
260
 
241
261
  const uriReferenceValidator: FormatValidatorFn = (uri: unknown) => {
242
- if (typeof uri !== 'string') return true;
262
+ if (typeof uri !== 'string') {
263
+ return true;
264
+ }
243
265
  // eslint-disable-next-line no-control-regex
244
- if (/[^\x00-\x7F]/.test(uri)) return false;
266
+ if (/[^\u0000-\u007F]/.test(uri)) {
267
+ return false;
268
+ }
245
269
  // URI-reference allows relative URIs
246
- return /^([a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^{}^`| ]*$/.test(uri);
270
+ return /^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^{}^`| ]*$/.test(uri);
247
271
  };
248
272
 
249
273
  const uriTemplateValidator: FormatValidatorFn = (uri: unknown) => {
250
- if (typeof uri !== 'string') return true;
274
+ if (typeof uri !== 'string') {
275
+ return true;
276
+ }
251
277
  // URI template allows braces for expressions.
252
- if (!/^([a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^`| ]*$/.test(uri)) {
278
+ if (!/^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^`| ]*$/.test(uri)) {
253
279
  return false;
254
280
  }
255
281
 
@@ -286,67 +312,127 @@ const hasValidTildeEscapes = (segment: string): boolean => {
286
312
  };
287
313
 
288
314
  const jsonPointerValidator: FormatValidatorFn = (pointer: unknown) => {
289
- if (typeof pointer !== 'string') return true;
315
+ if (typeof pointer !== 'string') {
316
+ return true;
317
+ }
290
318
  // JSON Pointer: empty, or a sequence of '/'-prefixed reference tokens.
291
319
  // In each token, '~' must be escaped as '~0' or '~1'.
292
- if (pointer === '') return true;
293
- if (!/^(?:\/[^/]*)+$/.test(pointer)) return false;
294
- const tokens = pointer.split('/').slice(1); // first element is empty before leading '/'
295
- for (const token of tokens) {
296
- if (!hasValidTildeEscapes(token)) return false;
320
+ if (pointer === '') {
321
+ return true;
322
+ }
323
+ if (!/^(?:\/[^/]*)+$/.test(pointer)) {
324
+ return false;
325
+ }
326
+ const tokens = pointer.split('/');
327
+ for (let i = 1; i < tokens.length; i++) {
328
+ if (!hasValidTildeEscapes(tokens[i])) {
329
+ return false;
330
+ }
297
331
  }
298
332
  return true;
299
333
  };
300
334
 
301
335
  const relativeJsonPointerValidator: FormatValidatorFn = (pointer: unknown) => {
302
- if (typeof pointer !== 'string') return true;
336
+ if (typeof pointer !== 'string') {
337
+ return true;
338
+ }
303
339
  // Relative JSON Pointer: non-negative integer prefix (no leading zeros unless zero),
304
340
  // followed by either '#', a JSON Pointer, or nothing.
305
- const match = pointer.match(/^(0|[1-9]\d*)(.*)$/);
306
- if (!match) return false;
341
+ const match = /^(?<int>0|[1-9]\d*)(?<suffix>.*)$/.exec(pointer);
342
+ if (!match) {
343
+ return false;
344
+ }
307
345
  const suffix = match[2];
308
- if (suffix === '' || suffix === '#') return true;
309
- if (!suffix.startsWith('/')) return false;
310
- if (!/^(?:\/[^/]*)+$/.test(suffix)) return false;
311
- const tokens = suffix.split('/').slice(1);
312
- for (const token of tokens) {
313
- if (!hasValidTildeEscapes(token)) return false;
346
+ if (suffix === '' || suffix === '#') {
347
+ return true;
348
+ }
349
+ if (!suffix.startsWith('/')) {
350
+ return false;
351
+ }
352
+ if (!/^(?:\/[^/]*)+$/.test(suffix)) {
353
+ return false;
354
+ }
355
+ const tokens = suffix.split('/');
356
+ for (let i = 1; i < tokens.length; i++) {
357
+ if (!hasValidTildeEscapes(tokens[i])) {
358
+ return false;
359
+ }
314
360
  }
315
361
  return true;
316
362
  };
317
363
 
318
364
  const timeValidator: FormatValidatorFn = (time: unknown) => {
319
- if (typeof time !== 'string') return true;
365
+ if (typeof time !== 'string') {
366
+ return true;
367
+ }
320
368
  return parseRfc3339Time(time) !== null;
321
369
  };
322
370
 
371
+ // Matches a lone (unpaired) UTF-16 surrogate — either a high surrogate not
372
+ // followed by a low surrogate, or a low surrogate not preceded by a high one.
373
+ const LONE_SURROGATE_REGEX = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]/;
374
+ // A quoted local part: any char except a bare quote/backslash; backslash escapes
375
+ // allowed. Leniently permits raw control chars (e.g. bare CR/LF), which stricter
376
+ // RFC 5321 parsing forbids — an accepted limitation, not exercised by the suite.
377
+ const QUOTED_LOCAL_PART_REGEX = /^"(?:[^"\\]|\\.)*"$/u;
378
+ const UNQUOTED_LOCAL_PART_REGEX = /^[^\s@]+$/u;
379
+
380
+ const isValidIdnEmailLocalPart = (localPart: string): boolean => {
381
+ if (localPart.length === 0 || LONE_SURROGATE_REGEX.test(localPart)) {
382
+ return false;
383
+ }
384
+ if (localPart.length >= 2 && localPart.startsWith('"') && localPart.endsWith('"')) {
385
+ return QUOTED_LOCAL_PART_REGEX.test(localPart);
386
+ }
387
+ return UNQUOTED_LOCAL_PART_REGEX.test(localPart);
388
+ };
389
+
323
390
  const idnEmailValidator: FormatValidatorFn = (email: unknown) => {
324
- if (typeof email !== 'string') return true;
325
- // Simple email check, allowing international chars
326
- return /^[^\s@]+@[^\s@]+$/.test(email);
391
+ if (typeof email !== 'string') {
392
+ return true;
393
+ }
394
+ // Split on the last '@': everything after it is the (idn-)hostname domain,
395
+ // everything before it is the local part (which may itself be quoted).
396
+ // Note: unlike the ASCII `email` validator, IP-literal domains
397
+ // (`user@[192.168.1.1]` / `@[IPv6:...]`) are not supported here.
398
+ const atIdx = email.lastIndexOf('@');
399
+ if (atIdx <= 0 || atIdx === email.length - 1) {
400
+ return false;
401
+ }
402
+ return isValidIdnEmailLocalPart(email.slice(0, atIdx)) && isValidIdnHostname(email.slice(atIdx + 1));
327
403
  };
328
404
 
329
405
  const idnHostnameValidator: FormatValidatorFn = (hostname: unknown) => {
330
- if (typeof hostname !== 'string') return true;
406
+ if (typeof hostname !== 'string') {
407
+ return true;
408
+ }
331
409
  return isValidIdnHostname(hostname);
332
410
  };
333
411
 
334
412
  const iriValidator: FormatValidatorFn = (iri: unknown) => {
335
- if (typeof iri !== 'string') return true;
413
+ if (typeof iri !== 'string') {
414
+ return true;
415
+ }
336
416
  if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:[^"\\<>^{}^`| ]*$/u.test(iri)) {
337
417
  return false;
338
418
  }
339
419
  try {
420
+ // Constructed purely to detect an unparseable IRI (throws). Avoids URL.canParse,
421
+ // which is unavailable in older browsers the UMD build still targets; no-new is
422
+ // disabled here as the construction is intentional and the result is unused.
423
+ // oxlint-disable-next-line no-new
340
424
  new URL(iri);
341
425
  return true;
342
- } catch (_e) {
426
+ } catch {
343
427
  return false;
344
428
  }
345
429
  };
346
430
 
347
431
  const iriReferenceValidator: FormatValidatorFn = (iriReference: unknown) => {
348
- if (typeof iriReference !== 'string') return true;
349
- return /^([a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^{}^`| ]*$/u.test(iriReference);
432
+ if (typeof iriReference !== 'string') {
433
+ return true;
434
+ }
435
+ return /^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^{}^`| ]*$/u.test(iriReference);
350
436
  };
351
437
 
352
438
  export interface FormatValidatorsOptions {
@@ -385,10 +471,24 @@ export function getFormatValidators(options?: FormatValidatorsOptions): Record<s
385
471
  ...inbuiltValidators,
386
472
  ...(options?.strictUris ? { uri: strictUriValidator } : {}),
387
473
  ...customValidators,
388
- ...(options?.customFormats || {}),
474
+ ...options?.customFormats,
389
475
  };
390
476
  }
391
477
 
478
+ export function resolveFormatValidator(name: string, options?: FormatValidatorsOptions): FormatValidatorFn | undefined {
479
+ const custom = options?.customFormats;
480
+ if (custom && Object.hasOwn(custom, name)) {
481
+ return custom[name] as FormatValidatorFn | undefined;
482
+ }
483
+ if (Object.hasOwn(customValidators, name)) {
484
+ return customValidators[name];
485
+ }
486
+ if (options?.strictUris && name === 'uri') {
487
+ return strictUriValidator;
488
+ }
489
+ return inbuiltValidators[name];
490
+ }
491
+
392
492
  export function registerFormat(name: string, validatorFunction: FormatValidatorFn) {
393
493
  customValidators[name] = validatorFunction;
394
494
  }
@@ -411,10 +511,16 @@ export function isFormatSupported(name: string, customFormats?: Record<string, F
411
511
  if (customFormats) {
412
512
  const custom = customFormats[name];
413
513
  // Explicitly null means unregistered at instance level
414
- if (custom === null) return false;
415
- if (custom != null) return true;
514
+ if (custom === null) {
515
+ return false;
516
+ }
517
+ if (custom != null) {
518
+ return true;
519
+ }
520
+ }
521
+ if (name in customValidators) {
522
+ return customValidators[name] != null;
416
523
  }
417
- if (name in customValidators) return customValidators[name] != null;
418
524
  return name in inbuiltValidators;
419
525
  }
420
526
 
@@ -75,11 +75,65 @@ export interface JsonSchemaAll extends JsonSchemaCommon {
75
75
  // Internal types (schema + z-schema runtime properties)
76
76
  // ---------------------------------------------------------------------------
77
77
 
78
+ /**
79
+ * Sub-schema-valued keywords. On the internal type these point at
80
+ * `JsonSchemaInternal` (rather than the public `JsonSchema`) so the validator
81
+ * can traverse nested schemas without asserting `as JsonSchemaInternal` at
82
+ * every step.
83
+ */
84
+ type InternalSchemaKeys =
85
+ | 'items'
86
+ | 'additionalItems'
87
+ | 'properties'
88
+ | 'patternProperties'
89
+ | 'additionalProperties'
90
+ | 'dependencies'
91
+ | 'allOf'
92
+ | 'anyOf'
93
+ | 'oneOf'
94
+ | 'not'
95
+ | 'definitions'
96
+ | 'contains'
97
+ | 'propertyNames'
98
+ | 'if'
99
+ | 'then'
100
+ | 'else'
101
+ | '$defs'
102
+ | 'dependentSchemas'
103
+ | 'unevaluatedItems'
104
+ | 'unevaluatedProperties'
105
+ | 'prefixItems';
106
+
78
107
  /**
79
108
  * Internal schema type used throughout the validator. Based on `JsonSchemaAll`
80
- * so that validators can access any draft-specific property without narrowing.
109
+ * so that validators can access any draft-specific property without narrowing,
110
+ * but with every sub-schema keyword re-typed to `JsonSchemaInternal` so nested
111
+ * traversal needs no type assertions.
81
112
  */
82
- export type JsonSchemaInternal = JsonSchemaAll & ZSchemaInternalProperties;
113
+ export type JsonSchemaInternal = Omit<JsonSchemaAll, InternalSchemaKeys> &
114
+ ZSchemaInternalProperties & {
115
+ items?: JsonSchemaInternal | boolean | Array<JsonSchemaInternal | boolean>;
116
+ additionalItems?: boolean | JsonSchemaInternal;
117
+ properties?: Record<string, JsonSchemaInternal | boolean>;
118
+ patternProperties?: Record<string, JsonSchemaInternal>;
119
+ additionalProperties?: boolean | JsonSchemaInternal;
120
+ dependencies?: Record<string, string[] | JsonSchemaInternal>;
121
+ allOf?: JsonSchemaInternal[];
122
+ anyOf?: JsonSchemaInternal[];
123
+ oneOf?: JsonSchemaInternal[];
124
+ not?: JsonSchemaInternal;
125
+ definitions?: Record<string, JsonSchemaInternal>;
126
+ contains?: JsonSchemaInternal;
127
+ propertyNames?: JsonSchemaInternal;
128
+ if?: JsonSchemaInternal | boolean;
129
+ then?: JsonSchemaInternal | boolean;
130
+ else?: JsonSchemaInternal | boolean;
131
+ $defs?: Record<string, JsonSchemaInternal>;
132
+ dependentSchemas?: Record<string, JsonSchemaInternal>;
133
+ unevaluatedItems?: JsonSchemaInternal | boolean;
134
+ unevaluatedProperties?: JsonSchemaInternal | boolean;
135
+ prefixItems?: Array<JsonSchemaInternal | boolean>;
136
+ };
83
137
 
84
138
  /** @deprecated Use `JsonSchemaInternal` — they are now equivalent. */
85
139
  export type JsonSchemaInternalAll = JsonSchemaAll & ZSchemaInternalProperties;
@@ -2,18 +2,19 @@ import type { JsonSchema, JsonSchemaInternal } from './json-schema-versions.js';
2
2
  import type { Reference } from './schema-compiler.js';
3
3
  import type { ZSchemaOptions } from './z-schema-options.js';
4
4
 
5
+ import { DEFAULT_MAX_RECURSION_DEPTH } from './utils/constants.js';
5
6
  import { getRemotePath, isAbsoluteUri } from './utils/uri.js';
7
+ import { isObject } from './utils/what-is.js';
6
8
 
7
9
  /**
8
10
  * Keywords whose values are not JSON Schema sub-schemas and must not be
9
11
  * traversed during schema walking (id collection, reference collection, etc.).
10
12
  */
11
13
  export const NON_SCHEMA_KEYWORDS = ['enum', 'const', 'default', 'examples'] as const;
14
+ export const NON_SCHEMA_KEYWORDS_SET = new Set<string>(NON_SCHEMA_KEYWORDS);
12
15
 
13
16
  /** Returns true if the key is an internal z-schema property (prefixed with `__$`). */
14
17
  export const isInternalKey = (key: string): boolean => key.startsWith('__$');
15
- import { DEFAULT_MAX_RECURSION_DEPTH } from './utils/constants.js';
16
- import { isObject } from './utils/what-is.js';
17
18
 
18
19
  /**
19
20
  * Properties present in ALL JSON Schema drafts (04 through 2020-12) with
@@ -30,7 +31,7 @@ export interface JsonSchemaCommon {
30
31
 
31
32
  // ── Type / enum ─────────────────────────────────────────────────────────
32
33
  type?: string | string[];
33
- enum?: Array<unknown>;
34
+ enum?: unknown[];
34
35
  format?: string;
35
36
 
36
37
  // ── Numeric ─────────────────────────────────────────────────────────────
@@ -78,9 +79,9 @@ export type JsonSchemaType = 'array' | 'boolean' | 'integer' | 'null' | 'number'
78
79
  export interface ZSchemaInternalProperties {
79
80
  __$compiled?: unknown;
80
81
  __$missingReferences?: Reference[];
81
- __$refResolved?: JsonSchema;
82
- __$dynamicRefResolved?: JsonSchema;
83
- __$recursiveRefResolved?: JsonSchema;
82
+ __$refResolved?: JsonSchemaInternal;
83
+ __$dynamicRefResolved?: JsonSchemaInternal;
84
+ __$recursiveRefResolved?: JsonSchemaInternal;
84
85
  __$resourceRoot?: JsonSchemaInternal;
85
86
  __$schemaResolved?: unknown;
86
87
  __$validated?: boolean;
@@ -142,7 +143,7 @@ export const findId = (
142
143
  const inTargetBase = !targetBaseUri || nextBaseUri === targetBaseUri;
143
144
 
144
145
  if (inTargetBase) {
145
- if (schemaId && (schemaId === id || (schemaId[0] === '#' && schemaId.substring(1) === id))) {
146
+ if (schemaId && (schemaId === id || (schemaId.startsWith('#') && schemaId.slice(1) === id))) {
146
147
  return schema;
147
148
  }
148
149
  if (schema.$anchor === id || schema.$dynamicAnchor === id) {
@@ -153,7 +154,7 @@ export const findId = (
153
154
  let result;
154
155
  if (Array.isArray(schema)) {
155
156
  for (let i = 0; i < schema.length; i++) {
156
- result = findId(schema[i], id, targetBaseUri, nextBaseUri, maxDepth, _depth + 1);
157
+ result = findId(schema[i] as JsonSchemaInternal, id, targetBaseUri, nextBaseUri, maxDepth, _depth + 1);
157
158
  if (result) {
158
159
  return result;
159
160
  }
@@ -167,7 +168,7 @@ export const findId = (
167
168
  // intermediate resources - pointer reference across resource boundary".
168
169
  for (let i = keys.length - 1; i >= 0; i--) {
169
170
  const k = keys[i];
170
- if (isInternalKey(k) || NON_SCHEMA_KEYWORDS.includes(k as any)) {
171
+ if (isInternalKey(k) || NON_SCHEMA_KEYWORDS_SET.has(k)) {
171
172
  continue;
172
173
  }
173
174
  result = findId(schema[k] as JsonSchemaInternal, id, targetBaseUri, nextBaseUri, maxDepth, _depth + 1);