schema-components 1.21.0 → 1.22.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 (72) hide show
  1. package/README.md +1 -1
  2. package/dist/core/adapter.d.mts +20 -3
  3. package/dist/core/adapter.mjs +209 -28
  4. package/dist/core/constraints.d.mts +2 -2
  5. package/dist/core/diagnostics.d.mts +1 -1
  6. package/dist/core/errors.d.mts +1 -1
  7. package/dist/core/fieldOrder.d.mts +1 -1
  8. package/dist/core/formats.d.mts +22 -1
  9. package/dist/core/formats.mjs +21 -0
  10. package/dist/core/limits.d.mts +2 -0
  11. package/dist/core/limits.mjs +23 -0
  12. package/dist/core/merge.d.mts +1 -1
  13. package/dist/core/normalise.d.mts +29 -3
  14. package/dist/core/normalise.mjs +2 -2
  15. package/dist/core/openapi30.mjs +1 -1
  16. package/dist/core/ref.d.mts +1 -1
  17. package/dist/core/ref.mjs +1 -0
  18. package/dist/core/renderer.d.mts +1 -1
  19. package/dist/core/renderer.mjs +0 -2
  20. package/dist/core/swagger2.d.mts +1 -1
  21. package/dist/core/swagger2.mjs +1 -1
  22. package/dist/core/typeInference.d.mts +2 -2
  23. package/dist/core/types.d.mts +2 -2
  24. package/dist/core/types.mjs +1 -4
  25. package/dist/core/version.d.mts +1 -1
  26. package/dist/core/walkBuilders.d.mts +3 -3
  27. package/dist/core/walker.d.mts +1 -1
  28. package/dist/core/walker.mjs +79 -2
  29. package/dist/{diagnostics-CbBPsxSt.d.mts → diagnostics-D0QCYGv0.d.mts} +1 -1
  30. package/dist/{errors-QEwOtQAA.d.mts → errors-DpFwqs5C.d.mts} +1 -1
  31. package/dist/html/a11y.d.mts +2 -2
  32. package/dist/html/a11y.mjs +10 -3
  33. package/dist/html/renderToHtml.d.mts +10 -3
  34. package/dist/html/renderToHtml.mjs +13 -3
  35. package/dist/html/renderToHtmlStream.d.mts +2 -2
  36. package/dist/html/renderers.d.mts +2 -2
  37. package/dist/html/renderers.mjs +0 -5
  38. package/dist/html/streamRenderers.d.mts +5 -4
  39. package/dist/html/streamRenderers.mjs +91 -30
  40. package/dist/limits-Cw5QZND8.d.mts +29 -0
  41. package/dist/{normalise-DaSrnr8g.mjs → normalise-DVEJQmF7.mjs} +468 -115
  42. package/dist/openapi/ApiCallbacks.d.mts +1 -1
  43. package/dist/openapi/ApiLinks.d.mts +1 -1
  44. package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
  45. package/dist/openapi/ApiSecurity.d.mts +1 -1
  46. package/dist/openapi/ApiSecurity.mjs +16 -2
  47. package/dist/openapi/components.d.mts +150 -18
  48. package/dist/openapi/components.mjs +129 -15
  49. package/dist/openapi/parser.d.mts +1 -1
  50. package/dist/openapi/parser.mjs +35 -3
  51. package/dist/openapi/resolve.d.mts +12 -5
  52. package/dist/openapi/resolve.mjs +183 -23
  53. package/dist/react/SchemaComponent.d.mts +100 -35
  54. package/dist/react/SchemaComponent.mjs +59 -45
  55. package/dist/react/SchemaView.d.mts +3 -3
  56. package/dist/react/SchemaView.mjs +2 -2
  57. package/dist/react/fieldPath.d.mts +1 -1
  58. package/dist/react/headless.d.mts +1 -1
  59. package/dist/react/headless.mjs +1 -2
  60. package/dist/react/headlessRenderers.d.mts +3 -4
  61. package/dist/react/headlessRenderers.mjs +10 -30
  62. package/dist/{ref-si8ViYun.d.mts → ref-D-_JBZkF.d.mts} +1 -1
  63. package/dist/{renderer-DI6ZYf7a.d.mts → renderer-BaRlQIuN.d.mts} +2 -2
  64. package/dist/themes/mantine.d.mts +1 -1
  65. package/dist/themes/mui.d.mts +1 -1
  66. package/dist/themes/radix.d.mts +1 -1
  67. package/dist/themes/shadcn.d.mts +1 -1
  68. package/dist/typeInference-DkcUHfaM.d.mts +982 -0
  69. package/dist/{types-BnxPEElk.d.mts → types-BrRMV0en.d.mts} +3 -10
  70. package/package.json +1 -3
  71. package/dist/typeInference-Bxw3NOG1.d.mts +0 -647
  72. /package/dist/{version-D-u7aMfy.d.mts → version-D2jfdX6E.d.mts} +0 -0
package/README.md CHANGED
@@ -16,7 +16,7 @@ Peer dependencies: `zod@^4.0.0`, `react@^18.0.0 || ^19.0.0`.
16
16
 
17
17
  ### Zod version requirement
18
18
 
19
- schema-components requires **Zod 4**. If you are on Zod 3, see the [Zod 4 migration guide](https://zod.dev/v4/migration). The library detects Zod 3 inputs and throws a descriptive error rather than silently misbehaving.
19
+ schema-components requires **Zod 4**. If you are on Zod 3, see the [Zod 4 migration guide](https://zod.dev/v4/migration). If a Zod 3 schema is passed (detected via `_def.typeName`), a descriptive `SchemaNormalisationError` is raised pointing at the Zod 4 migration guide. Schemas from other Standard Schema libraries are not currently supported.
20
20
 
21
21
  ## `SchemaComponent`
22
22
 
@@ -1,9 +1,26 @@
1
- import { T as SchemaMeta, m as JsonObject } from "../types-BnxPEElk.mjs";
2
- import { i as DiagnosticsOptions } from "../diagnostics-CbBPsxSt.mjs";
1
+ import { m as JsonObject, w as SchemaMeta } from "../types-BrRMV0en.mjs";
2
+ import { i as DiagnosticsOptions } from "../diagnostics-D0QCYGv0.mjs";
3
3
 
4
4
  //#region src/core/adapter.d.ts
5
5
  type SchemaInput = Record<string, unknown>;
6
- type SchemaKind = "zod4" | "zod3" | "jsonSchema" | "openapi";
6
+ type SchemaKind = "zod4" | "zod3" | "jsonSchema" | "openapi" | "unsupported-schema-lib";
7
+ /**
8
+ * Classify the input schema by its structural markers.
9
+ *
10
+ * - `zod4` — has a `_zod` marker (further validation that `_zod.def` is a
11
+ * non-null object happens inside `normaliseZod4`).
12
+ * - `zod3` — has `_def` and no `_zod`. The `typeName` field is no longer
13
+ * required: any `_def` without `_zod` is treated as a probable Zod 3
14
+ * schema. Third-party libraries that expose `_def` without `_zod` are
15
+ * nearly always Zod 3 forks; surfacing the migration message is the
16
+ * correct response.
17
+ * - `openapi` — has `openapi` or `swagger` at the root.
18
+ * - `unsupported-schema-lib` — has `parse` and `safeParse` callables but
19
+ * no `_zod` and no `_def` marker. This catches Standard Schema
20
+ * implementations (valibot, arktype, etc.) that would otherwise flow
21
+ * through as "malformed JSON Schema".
22
+ * - `jsonSchema` — fallback for anything that does not match the above.
23
+ */
7
24
  declare function detectSchemaKind(input: unknown): SchemaKind;
8
25
  /**
9
26
  * Exposed for unit testing — lets the contract test enumerate every rule's
@@ -1,9 +1,10 @@
1
1
  import { getProperty, hasProperty, isObject } from "./guards.mjs";
2
+ import "./limits.mjs";
2
3
  import { SchemaNormalisationError } from "./errors.mjs";
3
4
  import { emitDiagnostic } from "./diagnostics.mjs";
4
5
  import { dereference } from "./ref.mjs";
5
6
  import { detectOpenApiVersion, inferJsonSchemaDraftWithReason, isSwagger2, matchJsonSchemaDraftUri } from "./version.mjs";
6
- import { a as normaliseOpenApiSchemas, i as normaliseJsonSchema$1 } from "../normalise-DaSrnr8g.mjs";
7
+ import { a as normaliseOpenApiSchemas, i as normaliseJsonSchema$1 } from "../normalise-DVEJQmF7.mjs";
7
8
  import { z } from "zod";
8
9
  //#region src/core/adapter.ts
9
10
  /**
@@ -18,13 +19,46 @@ import { z } from "zod";
18
19
  * All narrowing uses type guards — no type assertions.
19
20
  */
20
21
  const schemaCache = /* @__PURE__ */ new WeakMap();
22
+ /**
23
+ * Classify the input schema by its structural markers.
24
+ *
25
+ * - `zod4` — has a `_zod` marker (further validation that `_zod.def` is a
26
+ * non-null object happens inside `normaliseZod4`).
27
+ * - `zod3` — has `_def` and no `_zod`. The `typeName` field is no longer
28
+ * required: any `_def` without `_zod` is treated as a probable Zod 3
29
+ * schema. Third-party libraries that expose `_def` without `_zod` are
30
+ * nearly always Zod 3 forks; surfacing the migration message is the
31
+ * correct response.
32
+ * - `openapi` — has `openapi` or `swagger` at the root.
33
+ * - `unsupported-schema-lib` — has `parse` and `safeParse` callables but
34
+ * no `_zod` and no `_def` marker. This catches Standard Schema
35
+ * implementations (valibot, arktype, etc.) that would otherwise flow
36
+ * through as "malformed JSON Schema".
37
+ * - `jsonSchema` — fallback for anything that does not match the above.
38
+ */
21
39
  function detectSchemaKind(input) {
22
40
  if (hasProperty(input, "_zod")) return "zod4";
23
41
  if (hasProperty(input, "_def") && !hasProperty(input, "_zod")) return "zod3";
24
42
  if (hasProperty(input, "openapi") || hasProperty(input, "swagger")) return "openapi";
43
+ if (isLikelyOtherSchemaLib(input)) return "unsupported-schema-lib";
25
44
  return "jsonSchema";
26
45
  }
27
46
  /**
47
+ * Heuristic: a non-Zod object exposing both `.parse` and `.safeParse` as
48
+ * callables is almost certainly an instance of a competing schema library
49
+ * (Standard Schema, valibot, arktype, etc.). schema-components requires
50
+ * Zod 4 throughout — surfacing the unsupported library by name beats
51
+ * letting the input drop through to the JSON Schema branch where it
52
+ * would fail as "malformed JSON Schema" without explanation.
53
+ */
54
+ function isLikelyOtherSchemaLib(input) {
55
+ if (!isObject(input)) return false;
56
+ if (hasProperty(input, "_zod") || hasProperty(input, "_def")) return false;
57
+ const parse = input.parse;
58
+ const safeParse = input.safeParse;
59
+ return typeof parse === "function" && typeof safeParse === "function";
60
+ }
61
+ /**
28
62
  * Wraps z.toJSONSchema() for a runtime-validated Zod schema.
29
63
  *
30
64
  * The _zod guard in normaliseZod4 has confirmed this is a valid Zod schema,
@@ -32,6 +66,28 @@ function detectSchemaKind(input) {
32
66
  * that z.toJSONSchema expects. This is the library boundary equivalent of
33
67
  * object → Record<string, unknown> — the type mismatch is genuinely unavoidable.
34
68
  *
69
+ * # Options
70
+ *
71
+ * `z.toJSONSchema` is invoked with an explicit options object rather than
72
+ * Zod's defaults so the conversion contract is pinned and stable:
73
+ *
74
+ * - `target: "draft-2020-12"` — matches the walker's draft target.
75
+ * - `unrepresentable: "throw"` — keeps the unrepresentable-type rules in
76
+ * the classifier table firing instead of silently emitting `{}`.
77
+ * - `cycles: "ref"` — converts cyclic graphs into $ref pairs rather than
78
+ * throwing. Cycles in user schemas surface through the walker's $ref
79
+ * resolution rather than the adapter.
80
+ * - `io: "output"` — convert the OUTPUT side of every transform / pipe /
81
+ * codec. The input side is invisible to the converted schema, even
82
+ * though `safeParse` on the same Zod schema consumes the input shape.
83
+ * For transforms this divergence is fatal and the call throws via
84
+ * `Transforms cannot be represented`; for `z.codec(...)` the call
85
+ * succeeds but only the output side is rendered. Consumers receive a
86
+ * `zod-codec-output-only` diagnostic in the codec case so the
87
+ * asymmetry is visible — see `screenPreConversion`.
88
+ *
89
+ * # Error classification
90
+ *
35
91
  * Any exception thrown by z.toJSONSchema is classified into a
36
92
  * SchemaNormalisationError so the caller does not have to re-parse error
37
93
  * message strings. The classification covers:
@@ -69,12 +125,76 @@ function detectSchemaKind(input) {
69
125
  */
70
126
  function callToJsonSchema(schema) {
71
127
  try {
72
- return z.toJSONSchema(schema);
128
+ return z.toJSONSchema(schema, {
129
+ target: "draft-2020-12",
130
+ unrepresentable: "throw",
131
+ cycles: "ref",
132
+ io: "output"
133
+ });
73
134
  } catch (err) {
74
135
  throw classifyZodConversionError(err, schema);
75
136
  }
76
137
  }
77
138
  /**
139
+ * Zod `def.type` tags that have no useful JSON Schema representation but
140
+ * do NOT throw when passed through `z.toJSONSchema`. Each tag is handled
141
+ * by Zod with a processor that silently rewrites the output:
142
+ *
143
+ * - `promise` — `promiseProcessor` unwraps the inner type, dropping the
144
+ * `Promise<...>` wrapper without any error. (`json-schema-processors.ts`,
145
+ * the body of `promiseProcessor` calls `process(def.innerType, ...)`.)
146
+ * schema-components considers this a silent shape mismatch — the input
147
+ * tree advertised a `Promise<T>` and the consumer would render `T`
148
+ * without ever being told the wrapping was lost.
149
+ *
150
+ * Detection happens BEFORE the call to `z.toJSONSchema` so the response is
151
+ * an immediate `SchemaNormalisationError` with `kind:
152
+ * "zod-type-unrepresentable"`, matching the philosophy of
153
+ * `UnrepresentableZodType` in `typeInference.ts` — these types are
154
+ * rejected, not coerced.
155
+ */
156
+ const PRECONVERSION_UNREPRESENTABLE_TAGS = new Map([["promise", "z.promise(T) cannot be represented in JSON Schema. Zod silently unwraps it to the inner type, which would leave the rendered schema out of sync with the source. Resolve the promise at the data boundary before passing the value to the component."]]);
157
+ /**
158
+ * Pre-conversion screening. Inspects the root `_zod.def.type` tag for
159
+ * known-problematic types that either silently misrender (handled via
160
+ * {@link PRECONVERSION_UNREPRESENTABLE_TAGS}, raising a
161
+ * `SchemaNormalisationError`) or render correctly but with consumer-visible
162
+ * caveats (codecs, raising a `zod-codec-output-only` diagnostic).
163
+ *
164
+ * Design choice: `z.never()` is NOT classified here. The Zod processor for
165
+ * `never` already produces `{ not: {} }`, which the walker understands via
166
+ * its `walkBooleanSchema(false)` branch (`walker.ts` boolean-schema
167
+ * handling). Throwing a `zod-type-unrepresentable` for `never` would break
168
+ * the legitimate "this field cannot hold any value" use case that the
169
+ * walker already supports. Documented for posterity so future passes do
170
+ * not "fix" it.
171
+ */
172
+ function screenPreConversion(input, def, diagnostics) {
173
+ const tag = def.type;
174
+ if (typeof tag !== "string") return;
175
+ const unrepresentableMessage = PRECONVERSION_UNREPRESENTABLE_TAGS.get(tag);
176
+ if (unrepresentableMessage !== void 0) throw new SchemaNormalisationError(unrepresentableMessage, input, "zod-type-unrepresentable", tag);
177
+ if (tag === "pipe" && isCodecSchema(input)) emitDiagnostic(diagnostics, {
178
+ code: "zod-codec-output-only",
179
+ message: "z.codec(...) was passed at the schema root. Only the OUTPUT side is rendered by schema-components; the input side may differ. If you intend to render the input side instead, restructure the codec so the input type is the rendered shape.",
180
+ pointer: "",
181
+ detail: { zodType: "codec" }
182
+ });
183
+ }
184
+ /**
185
+ * True when `input` is a `z.codec(...)` instance. Detection looks for the
186
+ * `$ZodCodec` entry in `_zod.traits` — the same marker `z.toJSONSchema`'s
187
+ * own `isTransforming` helper uses to distinguish codecs from generic
188
+ * pipes.
189
+ */
190
+ function isCodecSchema(input) {
191
+ const zod = getProperty(input, "_zod");
192
+ if (!isObject(zod)) return false;
193
+ const traits = zod.traits;
194
+ if (traits instanceof Set) return traits.has("$ZodCodec");
195
+ return false;
196
+ }
197
+ /**
78
198
  * Escape a string for inclusion in a `RegExp`. Required because Zod
79
199
  * messages contain `[`, `]`, `.`, `(`, and `)` characters which have regex
80
200
  * meaning. The set covers every character with special meaning in a
@@ -106,14 +226,27 @@ function unrepresentableMessage(typeName, fullMessage) {
106
226
  * test suite asserts no two `prefix` values are prefixes of each other —
107
227
  * any future rule that breaks the invariant fails the build.
108
228
  *
109
- * Verbatim sources (kept aligned with `tests/zod-error-wording-contract.unit.test.ts`):
110
- * - zod/src/v4/core/json-schema-processors.ts L104 (bigint), L110 (symbol),
111
- * L126 (undefined), L132 (void), L150 (date), L169 (literal-undefined),
112
- * L175 (literal-bigint), L204 (NaN), L246 (custom), L252 (function),
113
- * L258 (transforms), L264 (map), L270 (set), L521 (dynamic catch).
114
- * - zod/src/v4/core/to-json-schema.ts L182 (non-representable type fallback),
115
- * L225 + L364 (unprocessed schema), L235 (duplicate id), L307 (cycle),
116
- * L522 (error converting).
229
+ * Verbatim sources (kept aligned with `tests/zod-error-wording-contract.unit.test.ts`).
230
+ * Source files are referenced by message-content anchors rather than line
231
+ * numbers line numbers drift across Zod patch releases but the message
232
+ * strings themselves are stable and protected by the contract test suite:
233
+ *
234
+ * - `zod/src/v4/core/json-schema-processors.ts` emits `BigInt cannot be
235
+ * represented`, `Symbols cannot be represented`, `Undefined cannot be
236
+ * represented`, `Void cannot be represented`, `Date cannot be
237
+ * represented`, `Literal \`undefined\` cannot be represented`,
238
+ * `BigInt literals cannot be represented`, `NaN cannot be represented`,
239
+ * `Custom types cannot be represented`, `Function types cannot be
240
+ * represented`, `Transforms cannot be represented`, `Map cannot be
241
+ * represented`, `Set cannot be represented`, `Dynamic catch values are
242
+ * not supported`.
243
+ * - `zod/src/v4/core/to-json-schema.ts` — emits `[toJSONSchema]:
244
+ * Non-representable type encountered: ${def.type}` (the catch-all
245
+ * fallback), `Unprocessed schema. This is a bug in Zod.` (the
246
+ * internal-bug branch), `Duplicate schema id "${id}" detected during
247
+ * JSON Schema conversion.` (the duplicate-id branch), `Cycle detected:
248
+ * ` (the cycle-throw branch), and `Error converting schema to JSON.`
249
+ * (the Standard Schema boundary wrapper).
117
250
  */
118
251
  const CLASSIFIER_RULES = [
119
252
  {
@@ -212,7 +345,7 @@ const CLASSIFIER_RULES = [
212
345
  prefix: "Cycle detected: ",
213
346
  kind: "zod-cycle-detected",
214
347
  build: (match, cause, schema, full) => {
215
- return new SchemaNormalisationError(`Zod detected a cycle in the schema graph at ${(match[1] ?? "").split(/\s+/)[0] ?? ""}. Cycles can only be converted when z.toJSONSchema is called with { cycles: "ref" } schema-components calls it without options for cache safety, so the cycle surfaces as an error. Restructure the schema to break the cycle, or use a $ref-based definition. Original message: ${full}`, schema, "zod-cycle-detected", void 0, cause);
348
+ return new SchemaNormalisationError(`Zod detected a cycle in the schema graph at ${(match[1] ?? "").split(/\s+/)[0] ?? ""}. schema-components calls z.toJSONSchema with { cycles: "ref" } so legitimate cyclic graphs convert to $ref pairs; this error surfaces only when Zod is unable to break the cycle even under the "ref" policy. Restructure the schema to break the cycle, or use an explicit $ref-based definition. Original message: ${full}`, schema, "zod-cycle-detected", void 0, cause);
216
349
  }
217
350
  },
218
351
  {
@@ -244,33 +377,57 @@ const COMPILED_CLASSIFIER_RULES = CLASSIFIER_RULES.map((rule) => ({
244
377
  pattern: anchored(rule.prefix)
245
378
  }));
246
379
  /**
247
- * Walk an arbitrary value looking for Zod 3 markers (`_def.typeName`).
248
- * Zod 4 schemas always carry a `_zod.def`; Zod 3 schemas carry `_def`
249
- * with a `typeName` field. Presence of the latter anywhere in the tree
250
- * means a Zod 3 schema was nested inside a Zod 4 input, which is what
251
- * trips the V8 `"Cannot read properties of undefined"` failure.
380
+ * Maximum recursion depth for {@link containsNestedZod3}. Reuses the
381
+ * shared {@link MAX_REF_DEPTH} so the runtime walk and the compile-time
382
+ * `DEFAULT_MAX_DEPTH` (type-aliased to the same value) stay in lockstep.
383
+ */
384
+ /**
385
+ * Walk an arbitrary value looking for Zod 3 markers (`_def` without
386
+ * `_zod`). Zod 4 schemas always carry `_zod.def`; Zod 3 schemas carry
387
+ * `_def` (with or without a `typeName` field — third-party Zod-3-style
388
+ * libraries occasionally omit `typeName`). Presence of `_def` without
389
+ * `_zod` anywhere in the tree means a Zod 3 (or Zod-3-like) schema was
390
+ * nested inside a Zod 4 input, which is what trips the V8
391
+ * `"Cannot read properties of undefined"` failure.
252
392
  *
253
393
  * Engine-agnostic by construction — the detector inspects schema shape
254
394
  * instead of pattern-matching against the runtime's TypeError message,
255
395
  * so it works equivalently under V8, JavaScriptCore (Bun/Safari), and
256
396
  * SpiderMonkey (Firefox) — none of which agree on the wording.
257
397
  *
258
- * The walk is bounded by an explicit `visited` set so cyclical references
259
- * cannot cause stack overflow. The recursion follows both array elements
260
- * and own enumerable properties of every object encountered.
398
+ * Performance shortcuts:
399
+ *
400
+ * - **Targeted descent into Zod 4 nodes.** Once a node is identified as a
401
+ * Zod 4 schema (`_zod.def` is an object), the only branch that can
402
+ * carry user-supplied sub-schemas is `_zod.def` itself. Zod's other
403
+ * internal members (`_zod.traits`, `_zod.parse`, `_zod.bag`, etc.) are
404
+ * implementation surface and never contain user schemas, so walking
405
+ * them on every conversion failure is wasted work. Switching to a
406
+ * targeted descent (only `_zod.def` plus the schema root's `_def`
407
+ * field) trims the walk dramatically.
408
+ * - **Depth cap.** Recursion is bounded by {@link MAX_REF_DEPTH}
409
+ * so a pathological schema graph cannot cause stack overflow. The
410
+ * `visited` set still defends against cyclic references; the depth
411
+ * cap defends against deep-but-acyclic trees.
261
412
  */
262
413
  function containsNestedZod3(value, visited) {
414
+ return containsNestedZod3Inner(value, visited, 0);
415
+ }
416
+ function containsNestedZod3Inner(value, visited, depth) {
417
+ if (depth >= 64) return false;
263
418
  if (value === null || typeof value !== "object") return false;
264
419
  if (visited.has(value)) return false;
265
420
  visited.add(value);
266
421
  if (Array.isArray(value)) {
267
- for (const item of value) if (containsNestedZod3(item, visited)) return true;
422
+ for (const item of value) if (containsNestedZod3Inner(item, visited, depth + 1)) return true;
268
423
  return false;
269
424
  }
270
425
  if (!isObject(value)) return false;
271
426
  const def = value._def;
272
- if (value._zod === void 0 && isObject(def) && typeof def.typeName === "string") return true;
273
- for (const key of Object.keys(value)) if (containsNestedZod3(value[key], visited)) return true;
427
+ const zod = value._zod;
428
+ if (zod === void 0 && isObject(def)) return true;
429
+ if (isObject(zod) && isObject(zod.def)) return containsNestedZod3Inner(zod.def, visited, depth + 1);
430
+ for (const key of Object.keys(value)) if (containsNestedZod3Inner(value[key], visited, depth + 1)) return true;
274
431
  return false;
275
432
  }
276
433
  function classifyZodConversionError(err, schema) {
@@ -288,7 +445,9 @@ function classifyZodConversionError(err, schema) {
288
445
  */
289
446
  const __CLASSIFIER_RULES_FOR_TEST = CLASSIFIER_RULES;
290
447
  function normaliseSchema(input, ref, options) {
291
- if (ref === void 0 && isObject(input)) {
448
+ const usesDiagnostics = options?.diagnostics !== void 0;
449
+ const cacheEligible = ref === void 0 && isObject(input) && !usesDiagnostics;
450
+ if (cacheEligible) {
292
451
  const cached = schemaCache.get(input);
293
452
  if (cached !== void 0) return cached;
294
453
  }
@@ -296,11 +455,12 @@ function normaliseSchema(input, ref, options) {
296
455
  let result;
297
456
  switch (kind) {
298
457
  case "zod4":
299
- result = normaliseZod4(input);
458
+ result = normaliseZod4(input, options?.diagnostics);
300
459
  break;
301
460
  case "zod3":
302
461
  result = normaliseZod3(input);
303
462
  break;
463
+ case "unsupported-schema-lib": throw new SchemaNormalisationError("Input looks like a schema from a non-Zod library — it exposes `parse` and `safeParse` but carries no Zod 4 (`_zod`) or Zod 3 (`_def`) marker. schema-components requires a Zod 4 schema. Convert the schema with the equivalent Zod 4 builder, or feed schema-components a JSON Schema / OpenAPI document instead. See the Zod 4 contract at https://zod.dev/v4 or run: pnpm add zod@^4", input, "unsupported-schema");
304
464
  case "openapi":
305
465
  if (!isObject(input)) throw new SchemaNormalisationError("Invalid OpenAPI document", input, "openapi-invalid");
306
466
  result = normaliseOpenApi(input, ref, options);
@@ -310,13 +470,15 @@ function normaliseSchema(input, ref, options) {
310
470
  result = normaliseJsonSchema(input, options?.diagnostics);
311
471
  break;
312
472
  }
313
- if (ref === void 0 && isObject(input)) schemaCache.set(input, result);
473
+ if (cacheEligible) schemaCache.set(input, result);
314
474
  return result;
315
475
  }
316
- function normaliseZod4(input) {
476
+ function normaliseZod4(input, diagnostics) {
317
477
  const zod = getProperty(input, "_zod");
318
- if (!isObject(zod)) throw new SchemaNormalisationError("Invalid Zod 4 schema: missing _zod property", input, "invalid-zod");
319
- if (!("def" in zod)) throw new SchemaNormalisationError("Invalid Zod 4 schema: missing _zod.def", input, "invalid-zod");
478
+ if (!isObject(zod)) throw new SchemaNormalisationError("Input is not a valid Zod 4 schema: `_zod` is present but is not an object. schema-components expected a Zod 4 schema produced by the `zod` package version 4 or later. See the Zod 4 migration guide at https://zod.dev/v4/migration or run: pnpm add zod@^4", input, "unsupported-schema");
479
+ const def = getProperty(zod, "def");
480
+ if (!isObject(def)) throw new SchemaNormalisationError("Input is not a valid Zod 4 schema: `_zod.def` is missing or not an object. schema-components expected a Zod 4 schema produced by the `zod` package version 4 or later. See the Zod 4 migration guide at https://zod.dev/v4/migration or run: pnpm add zod@^4", input, "unsupported-schema");
481
+ screenPreConversion(input, def, diagnostics);
320
482
  const jsonSchema = callToJsonSchema(input);
321
483
  if (!isObject(jsonSchema)) throw new SchemaNormalisationError("z.toJSONSchema() did not produce an object", input, "invalid-zod");
322
484
  return {
@@ -440,6 +602,23 @@ function resolveOpenApiRef(doc, ref) {
440
602
  }
441
603
  throw new Error(`Unsupported OpenAPI ref format: ${ref}`);
442
604
  }
605
+ /**
606
+ * Surface root-level metadata from the JSON Schema into the `rootMeta`
607
+ * shape consumed by the walker. Pulls `readOnly`, `writeOnly`,
608
+ * `description`, `title`, `deprecated`, `examples`, and `default`
609
+ * directly from the schema root.
610
+ *
611
+ * `examples` is forwarded only when present as an array (per JSON Schema
612
+ * Draft 2020-12 — Draft 04's `example` singular is normalised upstream).
613
+ * `default` is forwarded for any value the schema declares (any JSON
614
+ * value, including `null` and `false`); the presence check uses `in`
615
+ * so a literal `false` or `null` default is preserved.
616
+ *
617
+ * `examples` and `default` ride on the `[key: string]: unknown` index
618
+ * signature of {@link SchemaMeta}. They are not declared as named fields
619
+ * on `SchemaMeta` because that type lives in `types.ts` and is shared
620
+ * with the walker; the index signature is the agreed extension point.
621
+ */
443
622
  function extractRootMetaFromJson(jsonSchema) {
444
623
  const meta = {};
445
624
  if (jsonSchema.readOnly === true) meta.readOnly = true;
@@ -447,6 +626,8 @@ function extractRootMetaFromJson(jsonSchema) {
447
626
  if (typeof jsonSchema.description === "string") meta.description = jsonSchema.description;
448
627
  if (typeof jsonSchema.title === "string") meta.title = jsonSchema.title;
449
628
  if (typeof jsonSchema.deprecated === "boolean") meta.deprecated = jsonSchema.deprecated;
629
+ if (Array.isArray(jsonSchema.examples)) meta.examples = jsonSchema.examples;
630
+ if ("default" in jsonSchema) meta.default = jsonSchema.default;
450
631
  return Object.keys(meta).length > 0 ? meta : void 0;
451
632
  }
452
633
  //#endregion
@@ -1,5 +1,5 @@
1
- import { D as StringConstraints, f as FileConstraints, t as ArrayConstraints, x as ObjectConstraints, y as NumberConstraints } from "../types-BnxPEElk.mjs";
2
- import { i as DiagnosticsOptions } from "../diagnostics-CbBPsxSt.mjs";
1
+ import { E as StringConstraints, f as FileConstraints, t as ArrayConstraints, x as ObjectConstraints, y as NumberConstraints } from "../types-BrRMV0en.mjs";
2
+ import { i as DiagnosticsOptions } from "../diagnostics-D0QCYGv0.mjs";
3
3
 
4
4
  //#region src/core/constraints.d.ts
5
5
  declare function extractStringConstraints(schema: Record<string, unknown>, diagnostics?: DiagnosticsOptions, pointer?: string): StringConstraints;
@@ -1,2 +1,2 @@
1
- import { a as appendPointer, i as DiagnosticsOptions, n as DiagnosticCode, o as emitDiagnostic, r as DiagnosticSink, t as Diagnostic } from "../diagnostics-CbBPsxSt.mjs";
1
+ import { a as appendPointer, i as DiagnosticsOptions, n as DiagnosticCode, o as emitDiagnostic, r as DiagnosticSink, t as Diagnostic } from "../diagnostics-D0QCYGv0.mjs";
2
2
  export { Diagnostic, DiagnosticCode, DiagnosticSink, DiagnosticsOptions, appendPointer, emitDiagnostic };
@@ -1,2 +1,2 @@
1
- import { i as SchemaRenderError, n as SchemaFieldError, r as SchemaNormalisationError, t as SchemaError } from "../errors-QEwOtQAA.mjs";
1
+ import { i as SchemaRenderError, n as SchemaFieldError, r as SchemaNormalisationError, t as SchemaError } from "../errors-DpFwqs5C.mjs";
2
2
  export { SchemaError, SchemaFieldError, SchemaNormalisationError, SchemaRenderError };
@@ -1,4 +1,4 @@
1
- import { M as WalkedField } from "../types-BnxPEElk.mjs";
1
+ import { j as WalkedField } from "../types-BrRMV0en.mjs";
2
2
 
3
3
  //#region src/core/fieldOrder.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { i as DiagnosticsOptions } from "../diagnostics-CbBPsxSt.mjs";
1
+ import { i as DiagnosticsOptions } from "../diagnostics-D0QCYGv0.mjs";
2
2
 
3
3
  //#region src/core/formats.d.ts
4
4
  /**
@@ -16,6 +16,27 @@ type FormatValidator = RegExp | ((value: string) => boolean);
16
16
  /**
17
17
  * Recognised JSON Schema formats with their validation patterns.
18
18
  * Unknown formats emit an `unknown-format` diagnostic and skip derivation.
19
+ *
20
+ * Draft origin reference — formats first standardised by each draft:
21
+ *
22
+ * - Draft 04: `date-time`, `email`, `hostname`, `ipv4`, `ipv6`, `uri`
23
+ * - Draft 06: `uri-reference`, `uri-template`, `json-pointer`
24
+ * - Draft 07: `date`, `time`, `idn-email`, `idn-hostname`, `iri`,
25
+ * `iri-reference`, `regex`, `relative-json-pointer`
26
+ * - Draft 2019-09: `duration`, `uuid`
27
+ * - Vocabulary extensions / non-standard: `binary` (OpenAPI), and the
28
+ * Zod-emitted formats `cuid`, `cuid2`, `nanoid`, `cidrv4`, `cidrv6`,
29
+ * `base64`, `base64url`, `e164`, `emoji`, `ulid`, `xid`, `ksuid`,
30
+ * `lowercase`, `uppercase`, `jwt`, `json-string`
31
+ *
32
+ * Policy: schema-components accepts ALL formats in ALL drafts. We do
33
+ * not reject (e.g.) `uri-reference` on a Draft 04 schema or `uuid` on
34
+ * a Draft 06 schema, even though the spec did not standardise those
35
+ * names until a later draft. This matches the behaviour of every
36
+ * mainstream JSON Schema validator (Ajv, jsonschema, etc.) and avoids
37
+ * spurious failures on legitimate real-world schemas that pre-date or
38
+ * post-date the dialect they declare. Authors who want strict draft-
39
+ * locked behaviour should validate with a dedicated meta-schema tool.
19
40
  */
20
41
  /**
21
42
  * Email format pattern, exported as a named const so callers that need a
@@ -11,6 +11,27 @@ const MAX_REGEX_PATTERN_LENGTH = 500;
11
11
  /**
12
12
  * Recognised JSON Schema formats with their validation patterns.
13
13
  * Unknown formats emit an `unknown-format` diagnostic and skip derivation.
14
+ *
15
+ * Draft origin reference — formats first standardised by each draft:
16
+ *
17
+ * - Draft 04: `date-time`, `email`, `hostname`, `ipv4`, `ipv6`, `uri`
18
+ * - Draft 06: `uri-reference`, `uri-template`, `json-pointer`
19
+ * - Draft 07: `date`, `time`, `idn-email`, `idn-hostname`, `iri`,
20
+ * `iri-reference`, `regex`, `relative-json-pointer`
21
+ * - Draft 2019-09: `duration`, `uuid`
22
+ * - Vocabulary extensions / non-standard: `binary` (OpenAPI), and the
23
+ * Zod-emitted formats `cuid`, `cuid2`, `nanoid`, `cidrv4`, `cidrv6`,
24
+ * `base64`, `base64url`, `e164`, `emoji`, `ulid`, `xid`, `ksuid`,
25
+ * `lowercase`, `uppercase`, `jwt`, `json-string`
26
+ *
27
+ * Policy: schema-components accepts ALL formats in ALL drafts. We do
28
+ * not reject (e.g.) `uri-reference` on a Draft 04 schema or `uuid` on
29
+ * a Draft 06 schema, even though the spec did not standardise those
30
+ * names until a later draft. This matches the behaviour of every
31
+ * mainstream JSON Schema validator (Ajv, jsonschema, etc.) and avoids
32
+ * spurious failures on legitimate real-world schemas that pre-date or
33
+ * post-date the dialect they declare. Authors who want strict draft-
34
+ * locked behaviour should validate with a dedicated meta-schema tool.
14
35
  */
15
36
  /**
16
37
  * Email format pattern, exported as a named const so callers that need a
@@ -0,0 +1,2 @@
1
+ import { i as MaxRefDepth, n as MAX_REF_DEPTH, r as MAX_RENDER_DEPTH, t as MAX_PATH_ITEM_REF_HOPS } from "../limits-Cw5QZND8.mjs";
2
+ export { MAX_PATH_ITEM_REF_HOPS, MAX_REF_DEPTH, MAX_RENDER_DEPTH, MaxRefDepth };
@@ -0,0 +1,23 @@
1
+ //#region src/core/limits.ts
2
+ /**
3
+ * Shared depth caps and hop counts used to bound recursion across
4
+ * schema-components. All numeric limits live here so the renderer, the
5
+ * ref resolver, the OpenAPI parser, and the type-level inference engine
6
+ * agree on the same constants.
7
+ */
8
+ /**
9
+ * Maximum recursion depth for the schema walker, the React renderers,
10
+ * the streaming HTML renderer, and the server-side renderer. Beyond
11
+ * this depth a recursion sentinel is emitted instead of further descent
12
+ * — the only safe response to a cyclic walked-field graph.
13
+ */
14
+ const MAX_RENDER_DEPTH = 10;
15
+ const MAX_REF_DEPTH = 64;
16
+ /**
17
+ * Maximum number of `$ref` hops permitted when walking a chain of
18
+ * OpenAPI Path Item Object references. Beyond this a
19
+ * `path-item-ref-too-deep` diagnostic is emitted and resolution stops.
20
+ */
21
+ const MAX_PATH_ITEM_REF_HOPS = 8;
22
+ //#endregion
23
+ export { MAX_PATH_ITEM_REF_HOPS, MAX_REF_DEPTH, MAX_RENDER_DEPTH };
@@ -1,4 +1,4 @@
1
- import { i as DiagnosticsOptions } from "../diagnostics-CbBPsxSt.mjs";
1
+ import { i as DiagnosticsOptions } from "../diagnostics-D0QCYGv0.mjs";
2
2
 
3
3
  //#region src/core/merge.d.ts
4
4
  /**
@@ -1,5 +1,5 @@
1
- import { i as DiagnosticsOptions } from "../diagnostics-CbBPsxSt.mjs";
2
- import { i as OpenApiVersionInfo, r as JsonSchemaDraft } from "../version-D-u7aMfy.mjs";
1
+ import { i as DiagnosticsOptions } from "../diagnostics-D0QCYGv0.mjs";
2
+ import { i as OpenApiVersionInfo, r as JsonSchemaDraft } from "../version-D2jfdX6E.mjs";
3
3
 
4
4
  //#region src/core/normalise.d.ts
5
5
  type NodeTransform = (node: Record<string, unknown>) => Record<string, unknown>;
@@ -20,10 +20,28 @@ declare function deepNormalise(schema: Record<string, unknown>, transform: NodeT
20
20
  * Carries the diagnostics sink and the JSON Pointer to the current
21
21
  * node so per-node transforms can emit pointer-accurate diagnostics
22
22
  * when they translate or reject legacy constructs.
23
+ *
24
+ * `documentHasDynamicAnchor` is set once at the entry point by scanning
25
+ * the input for any `$dynamicAnchor`/`$recursiveAnchor` keyword. Per-
26
+ * node transforms read it to decide whether a `$dynamicRef`/`$recursiveRef`
27
+ * rewrite needs a `dynamic-ref-degraded` diagnostic — a ref pointing at
28
+ * an anchor in the document body cannot be statically resolved without
29
+ * losing dynamic-scope semantics, while a document with no dynamic
30
+ * anchors at all has no semantics to lose.
31
+ *
32
+ * `documentHasRecursiveAnchor` is the same idea for Draft 2019-09's
33
+ * `$recursiveAnchor` keyword.
34
+ *
35
+ * `declaredDraft` carries the draft the normaliser is operating under
36
+ * (when known) so per-node transforms can emit `keyword-out-of-draft`
37
+ * diagnostics for keywords introduced in a later draft.
23
38
  */
24
39
  interface NodeContext {
25
40
  diagnostics: DiagnosticsOptions | undefined;
26
41
  pointer: string;
42
+ documentHasDynamicAnchor: boolean;
43
+ documentHasRecursiveAnchor: boolean;
44
+ declaredDraft: JsonSchemaDraft | undefined;
27
45
  }
28
46
  type NodeTransformWithContext = (node: Record<string, unknown>, ctx: NodeContext) => Record<string, unknown>;
29
47
  /**
@@ -58,6 +76,14 @@ declare function deepNormaliseWithContext(schema: Record<string, unknown>, trans
58
76
  * {@link deepNormaliseWithContext} to thread diagnostics.
59
77
  */
60
78
  declare function normaliseDraft04Node(node: Record<string, unknown>): Record<string, unknown>;
79
+ /**
80
+ * Pick the per-node transform that normalises a single Schema Object to
81
+ * canonical Draft 2020-12 form for the supplied draft. Exposed so the
82
+ * OpenAPI 3.1 path can honour a non-default `jsonSchemaDialect`
83
+ * declaration by routing each Schema Object through the matching
84
+ * transform without re-implementing the dispatch.
85
+ */
86
+ declare function selectDraftTransform(draft: JsonSchemaDraft): NodeTransformWithContext;
61
87
  /**
62
88
  * Normalise a JSON Schema to canonical Draft 2020-12 form.
63
89
  * Deep-clones the input — the original is never mutated.
@@ -77,4 +103,4 @@ declare function normaliseJsonSchema(schema: Record<string, unknown>, draft: Jso
77
103
  */
78
104
  declare function normaliseOpenApiSchemas(doc: Record<string, unknown>, version: OpenApiVersionInfo, diagnostics?: DiagnosticsOptions): Record<string, unknown>;
79
105
  //#endregion
80
- export { NodeContext, NodeTransform, NodeTransformWithContext, deepNormalise, deepNormaliseWithContext, normaliseDraft04Node, normaliseJsonSchema, normaliseOpenApiSchemas };
106
+ export { NodeContext, NodeTransform, NodeTransformWithContext, deepNormalise, deepNormaliseWithContext, normaliseDraft04Node, normaliseJsonSchema, normaliseOpenApiSchemas, selectDraftTransform };
@@ -1,2 +1,2 @@
1
- import { a as normaliseOpenApiSchemas, i as normaliseJsonSchema, n as deepNormaliseWithContext, r as normaliseDraft04Node, t as deepNormalise } from "../normalise-DaSrnr8g.mjs";
2
- export { deepNormalise, deepNormaliseWithContext, normaliseDraft04Node, normaliseJsonSchema, normaliseOpenApiSchemas };
1
+ import { a as normaliseOpenApiSchemas, i as normaliseJsonSchema, n as deepNormaliseWithContext, o as selectDraftTransform, r as normaliseDraft04Node, t as deepNormalise } from "../normalise-DVEJQmF7.mjs";
2
+ export { deepNormalise, deepNormaliseWithContext, normaliseDraft04Node, normaliseJsonSchema, normaliseOpenApiSchemas, selectDraftTransform };
@@ -1,2 +1,2 @@
1
- import { c as deepNormaliseOpenApi30Doc, d as normaliseOpenApi30Discriminator, f as normaliseOpenApi30Node, l as deepNormaliseOpenApiDoc, s as applyDiscriminatorAllOfPrepass, u as normaliseOpenApi30Combined } from "../normalise-DaSrnr8g.mjs";
1
+ import { c as applyDiscriminatorAllOfPrepass, d as normaliseOpenApi30Combined, f as normaliseOpenApi30Discriminator, l as deepNormaliseOpenApi30Doc, p as normaliseOpenApi30Node, u as deepNormaliseOpenApiDoc } from "../normalise-DVEJQmF7.mjs";
2
2
  export { applyDiscriminatorAllOfPrepass, deepNormaliseOpenApi30Doc, deepNormaliseOpenApiDoc, normaliseOpenApi30Combined, normaliseOpenApi30Discriminator, normaliseOpenApi30Node };
@@ -1,2 +1,2 @@
1
- import { a as findAnchor, i as dereference, n as RefOptions, o as resolveRef, r as countDistinctRefs, t as ExternalResolver } from "../ref-si8ViYun.mjs";
1
+ import { a as findAnchor, i as dereference, n as RefOptions, o as resolveRef, r as countDistinctRefs, t as ExternalResolver } from "../ref-D-_JBZkF.mjs";
2
2
  export { ExternalResolver, RefOptions, countDistinctRefs, dereference, findAnchor, resolveRef };
package/dist/core/ref.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isObject } from "./guards.mjs";
2
+ import "./limits.mjs";
2
3
  import { emitDiagnostic } from "./diagnostics.mjs";
3
4
  import { isPrototypePollutingKey } from "./uri.mjs";
4
5
  //#region src/core/ref.ts
@@ -1,2 +1,2 @@
1
- import { a as HtmlRenderProps, c as RenderFunction, d as getHtmlRenderFn, f as getRenderFunction, h as typeToKey, i as HtmlRenderFunction, l as RenderProps, m as mergeResolvers, n as BaseFieldProps, o as HtmlResolver, p as mergeHtmlResolvers, r as ComponentResolver, s as RESOLVER_KEYS, t as AllConstraints, u as buildRenderProps } from "../renderer-DI6ZYf7a.mjs";
1
+ import { a as HtmlRenderProps, c as RenderFunction, d as getHtmlRenderFn, f as getRenderFunction, h as typeToKey, i as HtmlRenderFunction, l as RenderProps, m as mergeResolvers, n as BaseFieldProps, o as HtmlResolver, p as mergeHtmlResolvers, r as ComponentResolver, s as RESOLVER_KEYS, t as AllConstraints, u as buildRenderProps } from "../renderer-BaRlQIuN.mjs";
2
2
  export { AllConstraints, BaseFieldProps, ComponentResolver, HtmlRenderFunction, HtmlRenderProps, HtmlResolver, RESOLVER_KEYS, RenderFunction, RenderProps, buildRenderProps, getHtmlRenderFn, getRenderFunction, mergeHtmlResolvers, mergeResolvers, typeToKey };
@@ -43,7 +43,6 @@ const RESOLVER_KEYS = [
43
43
  "discriminatedUnion",
44
44
  "conditional",
45
45
  "negation",
46
- "recursive",
47
46
  "literal",
48
47
  "file",
49
48
  "never",
@@ -70,7 +69,6 @@ function typeToKey(type) {
70
69
  case "discriminatedUnion":
71
70
  case "conditional":
72
71
  case "negation":
73
- case "recursive":
74
72
  case "literal":
75
73
  case "file":
76
74
  case "never":
@@ -1,4 +1,4 @@
1
- import { i as DiagnosticsOptions } from "../diagnostics-CbBPsxSt.mjs";
1
+ import { i as DiagnosticsOptions } from "../diagnostics-D0QCYGv0.mjs";
2
2
  import { NodeTransform } from "./normalise.mjs";
3
3
 
4
4
  //#region src/core/swagger2.d.ts