schema-components 1.22.0 → 1.23.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 (82) hide show
  1. package/README.md +3 -1
  2. package/dist/core/adapter.d.mts +97 -3
  3. package/dist/core/adapter.mjs +260 -111
  4. package/dist/core/constraints.d.mts +2 -2
  5. package/dist/core/constraints.mjs +0 -7
  6. package/dist/core/cssClasses.d.mts +52 -0
  7. package/dist/core/cssClasses.mjs +51 -0
  8. package/dist/core/diagnostics.d.mts +1 -1
  9. package/dist/core/errors.d.mts +1 -1
  10. package/dist/core/errors.mjs +5 -13
  11. package/dist/core/fieldOrder.d.mts +1 -1
  12. package/dist/core/formats.d.mts +9 -2
  13. package/dist/core/formats.mjs +12 -1
  14. package/dist/core/idPath.d.mts +54 -0
  15. package/dist/core/idPath.mjs +66 -0
  16. package/dist/core/merge.d.mts +10 -1
  17. package/dist/core/merge.mjs +49 -10
  18. package/dist/core/normalise.d.mts +14 -3
  19. package/dist/core/normalise.mjs +2 -2
  20. package/dist/core/openapi30.d.mts +15 -1
  21. package/dist/core/openapi30.mjs +2 -2
  22. package/dist/core/openapiConstants.d.mts +67 -0
  23. package/dist/core/openapiConstants.mjs +90 -0
  24. package/dist/core/ref.d.mts +2 -2
  25. package/dist/core/ref.mjs +84 -6
  26. package/dist/core/refChain.d.mts +70 -0
  27. package/dist/core/refChain.mjs +44 -0
  28. package/dist/core/renderer.d.mts +1 -1
  29. package/dist/core/swagger2.d.mts +1 -1
  30. package/dist/core/swagger2.mjs +1 -1
  31. package/dist/core/typeInference.d.mts +982 -2
  32. package/dist/core/types.d.mts +1 -1
  33. package/dist/core/unionMatch.d.mts +36 -0
  34. package/dist/core/unionMatch.mjs +53 -0
  35. package/dist/core/version.d.mts +1 -1
  36. package/dist/core/version.mjs +29 -17
  37. package/dist/core/walkBuilders.d.mts +23 -4
  38. package/dist/core/walkBuilders.mjs +27 -7
  39. package/dist/core/walker.d.mts +1 -1
  40. package/dist/core/walker.mjs +44 -45
  41. package/dist/{diagnostics-D0QCYGv0.d.mts → diagnostics-BS2kaUyE.d.mts} +1 -1
  42. package/dist/{errors-DpFwqs5C.d.mts → errors-g_MCTQel.d.mts} +9 -15
  43. package/dist/html/a11y.d.mts +9 -4
  44. package/dist/html/a11y.mjs +10 -19
  45. package/dist/html/renderToHtml.d.mts +2 -2
  46. package/dist/html/renderToHtmlStream.d.mts +2 -2
  47. package/dist/html/renderToHtmlStream.mjs +12 -1
  48. package/dist/html/renderers.d.mts +43 -8
  49. package/dist/html/renderers.mjs +136 -111
  50. package/dist/html/streamRenderers.d.mts +4 -5
  51. package/dist/html/streamRenderers.mjs +40 -61
  52. package/dist/{normalise-DVEJQmF7.mjs → normalise-DCYp06Sr.mjs} +352 -162
  53. package/dist/openapi/ApiCallbacks.d.mts +1 -1
  54. package/dist/openapi/ApiLinks.d.mts +1 -1
  55. package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
  56. package/dist/openapi/ApiSecurity.d.mts +1 -1
  57. package/dist/openapi/components.d.mts +116 -37
  58. package/dist/openapi/components.mjs +54 -37
  59. package/dist/openapi/parser.d.mts +9 -8
  60. package/dist/openapi/parser.mjs +234 -84
  61. package/dist/openapi/resolve.d.mts +20 -11
  62. package/dist/openapi/resolve.mjs +133 -73
  63. package/dist/react/SchemaComponent.d.mts +32 -7
  64. package/dist/react/SchemaComponent.mjs +45 -21
  65. package/dist/react/SchemaView.d.mts +30 -10
  66. package/dist/react/a11y.d.mts +21 -0
  67. package/dist/react/a11y.mjs +24 -0
  68. package/dist/react/fieldPath.d.mts +1 -1
  69. package/dist/react/headless.d.mts +1 -1
  70. package/dist/react/headlessRenderers.d.mts +8 -9
  71. package/dist/react/headlessRenderers.mjs +41 -72
  72. package/dist/{ref-D-_JBZkF.d.mts → ref-DjLEKa_E.d.mts} +38 -3
  73. package/dist/{renderer-BaRlQIuN.d.mts → renderer-CXJ8y0qw.d.mts} +1 -1
  74. package/dist/themes/mantine.d.mts +1 -1
  75. package/dist/themes/mui.d.mts +1 -1
  76. package/dist/themes/radix.d.mts +1 -1
  77. package/dist/themes/shadcn.d.mts +1 -1
  78. package/dist/themes/shadcn.mjs +2 -1
  79. package/dist/{types-BrRMV0en.d.mts → types-BTB73MB8.d.mts} +32 -4
  80. package/dist/{version-D2jfdX6E.d.mts → version-BFTVLsdb.d.mts} +7 -1
  81. package/package.json +1 -1
  82. package/dist/typeInference-DkcUHfaM.d.mts +0 -982
package/README.md CHANGED
@@ -16,7 +16,9 @@ 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). 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.
19
+ schema-components requires **Zod 4**. If you are on Zod 3, see the [Zod 4 migration guide](https://zod.dev/v4/migration). Zod 3 schemas are detected structurally — any object exposing `_def` without the Zod 4 `_zod` marker is classified as Zod 3, with or without the historical `_def.typeName` field. (Some third-party Zod-3-style libraries omit `typeName`; the detector keys on the presence of `_def` alone.) A descriptive `SchemaNormalisationError` is raised pointing at the Zod 4 migration guide.
20
+
21
+ Schemas from other libraries that conform to the [Standard Schema](https://standardschema.dev/) spec (valibot, arktype, ...) are also detected and rejected. When the input advertises a `~standard.vendor` field, the error message includes the vendor name so consumers know which library produced the input.
20
22
 
21
23
  ## `SchemaComponent`
22
24
 
@@ -1,5 +1,5 @@
1
- import { m as JsonObject, w as SchemaMeta } from "../types-BrRMV0en.mjs";
2
- import { i as DiagnosticsOptions } from "../diagnostics-D0QCYGv0.mjs";
1
+ import { m as JsonObject, w as SchemaMeta } from "../types-BTB73MB8.mjs";
2
+ import { i as DiagnosticsOptions } from "../diagnostics-BS2kaUyE.mjs";
3
3
 
4
4
  //#region src/core/adapter.d.ts
5
5
  type SchemaInput = Record<string, unknown>;
@@ -22,6 +22,82 @@ type SchemaKind = "zod4" | "zod3" | "jsonSchema" | "openapi" | "unsupported-sche
22
22
  * - `jsonSchema` — fallback for anything that does not match the above.
23
23
  */
24
24
  declare function detectSchemaKind(input: unknown): SchemaKind;
25
+ /**
26
+ * Wraps z.toJSONSchema() for a runtime-validated Zod schema.
27
+ *
28
+ * The _zod guard in normaliseZod4 has confirmed this is a valid Zod schema,
29
+ * but TypeScript cannot represent "has _zod.def" as the $ZodType parameter
30
+ * that z.toJSONSchema expects. This is the library boundary equivalent of
31
+ * object → Record<string, unknown> — the type mismatch is genuinely unavoidable.
32
+ *
33
+ * # Options
34
+ *
35
+ * `z.toJSONSchema` is invoked with an explicit options object rather than
36
+ * Zod's defaults so the conversion contract is pinned and stable:
37
+ *
38
+ * - `target: "draft-2020-12"` — matches the walker's draft target.
39
+ * - `unrepresentable: "throw"` — keeps the unrepresentable-type rules in
40
+ * the classifier table firing instead of silently emitting `{}`.
41
+ * - `cycles: "ref"` — converts cyclic graphs into $ref pairs rather than
42
+ * throwing. Cycles in user schemas surface through the walker's $ref
43
+ * resolution rather than the adapter.
44
+ * - `io` — selects which side of every transform / pipe / codec is
45
+ * converted. Defaults to `"output"` (the OUTPUT side); pass `"input"`
46
+ * to render the INPUT side instead. The input side is invisible to
47
+ * the converted schema when `io: "output"` is in force, even though
48
+ * `safeParse` on the same Zod schema consumes the input shape. For
49
+ * transforms this divergence is fatal and the call throws via
50
+ * `Transforms cannot be represented`; for `z.codec(...)` the call
51
+ * succeeds but only the selected side is rendered. Consumers receive
52
+ * a `zod-codec-output-only` diagnostic in the codec case so the
53
+ * asymmetry is visible — see `screenPreConversion`.
54
+ *
55
+ * # Error classification
56
+ *
57
+ * Any exception thrown by z.toJSONSchema is classified into a
58
+ * SchemaNormalisationError so the caller does not have to re-parse error
59
+ * message strings. The classification covers:
60
+ *
61
+ * - Nested Zod 3 schemas inside a Zod 4 tree → zod3-unsupported.
62
+ * Detected structurally (presence of `_def.typeName` markers anywhere
63
+ * in the schema tree) so the check works across V8, JavaScriptCore,
64
+ * and SpiderMonkey, none of which agree on the wording of
65
+ * "Cannot read properties of undefined".
66
+ * - Transforms → zod-transform-unsupported. This also catches `z.codec(…)`
67
+ * because Zod implements codecs as a pipe + transform internally, so
68
+ * they trip the same processor when round-tripping is forced. (Plain
69
+ * `z.toJSONSchema(codec)` itself does NOT throw because Zod picks one
70
+ * side of the codec; the static rejection in `typeInference.ts` is the
71
+ * compile-time guard.)
72
+ * - Dynamic catch values whose handler throws → zod-type-unrepresentable
73
+ * with zodType "dynamic-catch".
74
+ * - Unrepresentable types — bigint, date, map, set, symbol, function, custom,
75
+ * undefined, void, NaN, and the literal-only forms `z.literal(undefined)`
76
+ * ("undefined-literal") and `z.literal(<bigint>)` ("bigint-literal") →
77
+ * zod-type-unrepresentable.
78
+ * - The catch-all "Non-representable type encountered: <type>" fallback Zod
79
+ * emits for any new schema kind without a registered processor →
80
+ * zod-type-unrepresentable with zodType set to the offending def.type.
81
+ * - Cycle detected (`cycles: "throw"`) → zod-cycle-detected.
82
+ * - Duplicate schema id → zod-duplicate-id.
83
+ * - "Unprocessed schema. This is a bug in Zod." → zod-conversion-bug.
84
+ * - "Error converting schema to JSON." → zod-conversion-failed (explicit
85
+ * classification rather than the generic fallback so the contract test
86
+ * protects the prefix from drift).
87
+ * - Anything else → zod-conversion-failed.
88
+ *
89
+ * The original error is preserved on each classified error via the `cause`
90
+ * field so consumers can still inspect the Zod stack trace.
91
+ */
92
+ /**
93
+ * IO side passed to {@link callToJsonSchema}. The Zod runtime accepts
94
+ * `"input" | "output"` for the corresponding `io` option on
95
+ * `z.toJSONSchema`. Defaults to `"output"` everywhere in the adapter
96
+ * pipeline; the parameter exists so a future renderer or component
97
+ * (currently SchemaComponent — see TODO below) can request the input
98
+ * side without forking the helper.
99
+ */
100
+ type SchemaIoSide = "input" | "output";
25
101
  /**
26
102
  * Exposed for unit testing — lets the contract test enumerate every rule's
27
103
  * `prefix` value and assert mutual non-prefixing.
@@ -44,5 +120,23 @@ interface NormaliseOptions {
44
120
  diagnostics?: DiagnosticsOptions;
45
121
  }
46
122
  declare function normaliseSchema(input: unknown, ref?: string, options?: NormaliseOptions): NormalisedSchema;
123
+ /**
124
+ * Surface root-level metadata from the JSON Schema into the `rootMeta`
125
+ * shape consumed by the walker. Pulls `readOnly`, `writeOnly`,
126
+ * `description`, `title`, `deprecated`, `examples`, and `default`
127
+ * directly from the schema root.
128
+ *
129
+ * `examples` is forwarded only when present as an array (per JSON Schema
130
+ * Draft 2020-12 — Draft 04's `example` singular is normalised upstream).
131
+ * `default` is forwarded for any value the schema declares (any JSON
132
+ * value, including `null` and `false`); the presence check uses `in`
133
+ * so a literal `false` or `null` default is preserved.
134
+ *
135
+ * `examples` and `default` ride on the `[key: string]: unknown` index
136
+ * signature of {@link SchemaMeta}. They are not declared as named fields
137
+ * on `SchemaMeta` because that type lives in `types.ts` and is shared
138
+ * with the walker; the index signature is the agreed extension point.
139
+ */
140
+ declare function extractRootMetaFromJson(jsonSchema: JsonObject): SchemaMeta | undefined;
47
141
  //#endregion
48
- export { type JsonObject, NormaliseOptions, NormalisedSchema, SchemaInput, SchemaKind, type SchemaMeta, __CLASSIFIER_RULES_FOR_TEST, detectSchemaKind, normaliseSchema };
142
+ export { type JsonObject, NormaliseOptions, NormalisedSchema, SchemaInput, SchemaIoSide, SchemaKind, type SchemaMeta, __CLASSIFIER_RULES_FOR_TEST, detectSchemaKind, extractRootMetaFromJson, normaliseSchema };
@@ -1,10 +1,10 @@
1
1
  import { getProperty, hasProperty, isObject } from "./guards.mjs";
2
2
  import "./limits.mjs";
3
3
  import { SchemaNormalisationError } from "./errors.mjs";
4
- import { emitDiagnostic } from "./diagnostics.mjs";
4
+ import { appendPointer, emitDiagnostic } from "./diagnostics.mjs";
5
5
  import { dereference } from "./ref.mjs";
6
6
  import { detectOpenApiVersion, inferJsonSchemaDraftWithReason, isSwagger2, matchJsonSchemaDraftUri } from "./version.mjs";
7
- import { a as normaliseOpenApiSchemas, i as normaliseJsonSchema$1 } from "../normalise-DVEJQmF7.mjs";
7
+ import { a as normaliseJsonSchema$1, o as normaliseOpenApiSchemas } from "../normalise-DCYp06Sr.mjs";
8
8
  import { z } from "zod";
9
9
  //#region src/core/adapter.ts
10
10
  /**
@@ -44,92 +44,49 @@ function detectSchemaKind(input) {
44
44
  return "jsonSchema";
45
45
  }
46
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.
47
+ * Heuristic: a non-Zod object that exposes either a Standard Schema
48
+ * `~standard.validate` entry point (valibot, arktype, and any pure
49
+ * Standard-Schema-conformant library) or both legacy `.parse`/`.safeParse`
50
+ * callables is almost certainly an instance of a competing schema
51
+ * library. schema-components requires Zod 4 throughout surfacing the
52
+ * unsupported library by name beats letting the input drop through to
53
+ * the JSON Schema branch where it would fail as "malformed JSON Schema"
54
+ * without explanation.
55
+ *
56
+ * Standard Schema detection takes priority: the spec mandates a
57
+ * `~standard` property carrying `{ validate, vendor, version }`. Pure
58
+ * Standard Schema implementations may not expose any `.parse`/`.safeParse`
59
+ * surface (those are a Zod / convenience API, not part of the spec), so
60
+ * the legacy heuristic alone would miss them. See
61
+ * https://standardschema.dev/ for the contract.
53
62
  */
54
63
  function isLikelyOtherSchemaLib(input) {
55
64
  if (!isObject(input)) return false;
56
65
  if (hasProperty(input, "_zod") || hasProperty(input, "_def")) return false;
66
+ if (isObject(input["~standard"])) return true;
57
67
  const parse = input.parse;
58
68
  const safeParse = input.safeParse;
59
69
  return typeof parse === "function" && typeof safeParse === "function";
60
70
  }
61
71
  /**
62
- * Wraps z.toJSONSchema() for a runtime-validated Zod schema.
63
- *
64
- * The _zod guard in normaliseZod4 has confirmed this is a valid Zod schema,
65
- * but TypeScript cannot represent "has _zod.def" as the $ZodType parameter
66
- * that z.toJSONSchema expects. This is the library boundary equivalent of
67
- * object Record<string, unknown> — the type mismatch is genuinely unavoidable.
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
- *
91
- * Any exception thrown by z.toJSONSchema is classified into a
92
- * SchemaNormalisationError so the caller does not have to re-parse error
93
- * message strings. The classification covers:
94
- *
95
- * - Nested Zod 3 schemas inside a Zod 4 tree → zod3-unsupported.
96
- * Detected structurally (presence of `_def.typeName` markers anywhere
97
- * in the schema tree) so the check works across V8, JavaScriptCore,
98
- * and SpiderMonkey, none of which agree on the wording of
99
- * "Cannot read properties of undefined".
100
- * - Transforms → zod-transform-unsupported. This also catches `z.codec(…)`
101
- * because Zod implements codecs as a pipe + transform internally, so
102
- * they trip the same processor when round-tripping is forced. (Plain
103
- * `z.toJSONSchema(codec)` itself does NOT throw because Zod picks one
104
- * side of the codec; the static rejection in `typeInference.ts` is the
105
- * compile-time guard.)
106
- * - Dynamic catch values whose handler throws → zod-type-unrepresentable
107
- * with zodType "dynamic-catch".
108
- * - Unrepresentable types — bigint, date, map, set, symbol, function, custom,
109
- * undefined, void, NaN, and the literal-only forms `z.literal(undefined)`
110
- * ("undefined-literal") and `z.literal(<bigint>)` ("bigint-literal") →
111
- * zod-type-unrepresentable.
112
- * - The catch-all "Non-representable type encountered: <type>" fallback Zod
113
- * emits for any new schema kind without a registered processor →
114
- * zod-type-unrepresentable with zodType set to the offending def.type.
115
- * - Cycle detected (`cycles: "throw"`) → zod-cycle-detected.
116
- * - Duplicate schema id → zod-duplicate-id.
117
- * - "Unprocessed schema. This is a bug in Zod." → zod-conversion-bug.
118
- * - "Error converting schema to JSON." → zod-conversion-failed (explicit
119
- * classification rather than the generic fallback so the contract test
120
- * protects the prefix from drift).
121
- * - Anything else → zod-conversion-failed.
122
- *
123
- * The original error is preserved on each classified error via the `cause`
124
- * field so consumers can still inspect the Zod stack trace.
72
+ * Extract the Standard Schema vendor string from a non-Zod input, when
73
+ * present. Returns `undefined` if the input does not advertise itself
74
+ * via the `~standard.vendor` field. Used to enrich the
75
+ * `unsupported-schema` error message with the library name so the
76
+ * consumer knows whether they have valibot, arktype, or another
77
+ * implementation in front of them.
125
78
  */
126
- function callToJsonSchema(schema) {
79
+ function extractStandardSchemaVendor(input) {
80
+ const vendor = getProperty(getProperty(input, "~standard"), "vendor");
81
+ return typeof vendor === "string" && vendor.length > 0 ? vendor : void 0;
82
+ }
83
+ function callToJsonSchema(schema, io = "output") {
127
84
  try {
128
85
  return z.toJSONSchema(schema, {
129
86
  target: "draft-2020-12",
130
87
  unrepresentable: "throw",
131
88
  cycles: "ref",
132
- io: "output"
89
+ io
133
90
  });
134
91
  } catch (err) {
135
92
  throw classifyZodConversionError(err, schema);
@@ -155,46 +112,205 @@ function callToJsonSchema(schema) {
155
112
  */
156
113
  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
114
  /**
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).
115
+ * Pre-conversion screening. Walks the entire Zod schema tree looking for
116
+ * silently-misrendered or caveat-bearing constructs and surfaces each as
117
+ * either a hard rejection (raised as a `SchemaNormalisationError`) or a
118
+ * diagnostic on the configured sink:
119
+ *
120
+ * - `z.promise(T)` at any depth → rejection (see
121
+ * {@link PRECONVERSION_UNREPRESENTABLE_TAGS}). Each nested occurrence
122
+ * first emits a `zod-promise-nested-unwrap` diagnostic so consumers
123
+ * with a sink see every offending location before the throw fires.
124
+ * The root occurrence still throws via the same path so behaviour is
125
+ * uniform regardless of position in the tree.
126
+ * - `z.codec(...)` at the root → `zod-codec-output-only` diagnostic.
127
+ * - `z.codec(...)` nested below the root →
128
+ * `zod-codec-nested-output-only` diagnostic per occurrence.
129
+ * - `z.preprocess(...)` at any depth → `zod-preprocess-output-only`
130
+ * diagnostic per occurrence. Preprocess never throws inside Zod (it
131
+ * silently rewrites to the output side), so the diagnostic is the
132
+ * only consumer-visible signal.
163
133
  *
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.
134
+ * Detection is structural — `_zod.def.type` plus `_zod.traits` (where
135
+ * present) and is depth-capped via {@link MAX_REF_DEPTH} with a
136
+ * `visited` set to defend against cyclic graphs. JSON-pointer fragments
137
+ * are accumulated as the walk descends so diagnostics report the exact
138
+ * subschema location rather than `""`.
139
+ *
140
+ * Design choice: `z.never()` is NOT classified here. The Zod processor
141
+ * for `never` already produces `{ not: {} }`, which the walker
142
+ * understands via its `walkBooleanSchema(false)` branch (`walker.ts`
143
+ * boolean-schema handling). Throwing a `zod-type-unrepresentable` for
144
+ * `never` would break the legitimate "this field cannot hold any value"
145
+ * use case that the walker already supports. Documented for posterity
146
+ * so future passes do not "fix" it.
171
147
  */
172
- function screenPreConversion(input, def, diagnostics) {
148
+ function screenPreConversion(input, diagnostics) {
149
+ let rejection;
150
+ screenPreConversionWalk(input, "", 0, true, /* @__PURE__ */ new Set(), diagnostics, (err) => {
151
+ rejection ??= err;
152
+ });
153
+ if (rejection !== void 0) throw rejection;
154
+ }
155
+ /**
156
+ * Inner recursion for {@link screenPreConversion}. Visits every Zod
157
+ * node reachable from `node`, emitting diagnostics and capturing
158
+ * rejections through `recordRejection`. The walk is targeted: only
159
+ * `_zod.def` is descended into (sibling `_zod.*` members are
160
+ * implementation surface and never carry user schemas — same rule as
161
+ * {@link containsNestedZod3Inner}).
162
+ *
163
+ * The `pointer` parameter tracks the JSON Pointer to the current
164
+ * subschema so diagnostics carry an accurate location. The `isRoot`
165
+ * flag distinguishes the entry call from recursive descents so
166
+ * `zod-codec-output-only` (root) and `zod-codec-nested-output-only`
167
+ * (nested) fire from the same code path.
168
+ */
169
+ function screenPreConversionWalk(node, pointer, depth, isRoot, visited, diagnostics, recordRejection) {
170
+ if (depth >= 64) return;
171
+ if (!isObject(node)) return;
172
+ if (visited.has(node)) return;
173
+ visited.add(node);
174
+ const zod = getProperty(node, "_zod");
175
+ if (!isObject(zod)) return;
176
+ const def = getProperty(zod, "def");
177
+ if (!isObject(def)) return;
173
178
  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, {
179
+ if (typeof tag === "string") {
180
+ const unrepresentableMessage = PRECONVERSION_UNREPRESENTABLE_TAGS.get(tag);
181
+ if (unrepresentableMessage !== void 0) {
182
+ if (tag === "promise") emitDiagnostic(diagnostics, {
183
+ code: "zod-promise-nested-unwrap",
184
+ message: `z.promise(...) detected at ${formatPointer(pointer)}. 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.`,
185
+ pointer,
186
+ detail: { zodType: "promise" }
187
+ });
188
+ recordRejection(new SchemaNormalisationError(unrepresentableMessage, node, "zod-type-unrepresentable", tag));
189
+ }
190
+ }
191
+ if (tag === "pipe" && hasTrait(zod, "$ZodCodec")) if (isRoot) emitDiagnostic(diagnostics, {
178
192
  code: "zod-codec-output-only",
179
193
  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: "",
194
+ pointer,
181
195
  detail: { zodType: "codec" }
182
196
  });
197
+ else emitDiagnostic(diagnostics, {
198
+ code: "zod-codec-nested-output-only",
199
+ message: `z.codec(...) detected at ${formatPointer(pointer)}. Only the OUTPUT side is rendered by schema-components; the input side is invisible to the converted schema even though safeParse still consumes the input shape.`,
200
+ pointer,
201
+ detail: { zodType: "codec" }
202
+ });
203
+ if (tag === "pipe" && hasTrait(zod, "$ZodPreprocess")) emitDiagnostic(diagnostics, {
204
+ code: "zod-preprocess-output-only",
205
+ message: `z.preprocess(...) detected at ${formatPointer(pointer)}. Zod silently renders the OUTPUT-side schema; the preprocess function and its input shape are invisible to the rendered schema. If you need the input shape, restructure the schema to declare it directly.`,
206
+ pointer,
207
+ detail: { zodType: "preprocess" }
208
+ });
209
+ screenPreConversionDescend(def, pointer, depth + 1, visited, diagnostics, recordRejection);
210
+ }
211
+ /**
212
+ * Descend into the values of a Zod `def` object, visiting every nested
213
+ * Zod schema. `def` shapes are heterogeneous, so we walk recursively
214
+ * through plain objects and arrays until we find a value with a
215
+ * `_zod.def` marker — those nodes are the user-supplied sub-schemas.
216
+ *
217
+ * Pointer accumulation:
218
+ *
219
+ * - For `def.shape.<key>` we emit pointers of the form
220
+ * `/properties/<key>`, matching the JSON Schema rendering of an
221
+ * object's properties so diagnostics line up with what consumers see
222
+ * in the rendered output.
223
+ * - For `def.items[<i>]` we emit `/items/<i>`.
224
+ * - For `def.options[<i>]` we emit `/anyOf/<i>` so union members line
225
+ * up with their JSON Schema position.
226
+ * - For pipe `def.in` / `def.out` we emit `/in` / `/out`.
227
+ * - Everything else descends without extending the pointer (the
228
+ * diagnostic stays anchored at the parent location).
229
+ *
230
+ * The pointer scheme is deliberately conservative — it errs on the
231
+ * side of "parent-anchored" when the JSON Schema name for a Zod field
232
+ * is ambiguous, rather than fabricating a synthetic location.
233
+ */
234
+ function screenPreConversionDescend(def, parentPointer, depth, visited, diagnostics, recordRejection) {
235
+ if (depth >= 64) return;
236
+ const shape = getProperty(def, "shape");
237
+ if (isObject(shape)) {
238
+ const shapeBase = appendPointer(parentPointer, "properties");
239
+ for (const [key, value] of Object.entries(shape)) screenPreConversionWalk(value, appendPointer(shapeBase, key), depth + 1, false, visited, diagnostics, recordRejection);
240
+ }
241
+ const items = getProperty(def, "items");
242
+ if (Array.isArray(items)) {
243
+ const itemsBase = appendPointer(parentPointer, "items");
244
+ items.forEach((item, index) => {
245
+ screenPreConversionWalk(item, appendPointer(itemsBase, String(index)), depth + 1, false, visited, diagnostics, recordRejection);
246
+ });
247
+ } else if (isObject(items)) screenPreConversionWalk(items, appendPointer(parentPointer, "items"), depth + 1, false, visited, diagnostics, recordRejection);
248
+ const options = getProperty(def, "options");
249
+ if (Array.isArray(options)) {
250
+ const optionsBase = appendPointer(parentPointer, "anyOf");
251
+ options.forEach((option, index) => {
252
+ screenPreConversionWalk(option, appendPointer(optionsBase, String(index)), depth + 1, false, visited, diagnostics, recordRejection);
253
+ });
254
+ }
255
+ const inSide = getProperty(def, "in");
256
+ if (isObject(inSide)) screenPreConversionWalk(inSide, appendPointer(parentPointer, "in"), depth + 1, false, visited, diagnostics, recordRejection);
257
+ const outSide = getProperty(def, "out");
258
+ if (isObject(outSide)) screenPreConversionWalk(outSide, appendPointer(parentPointer, "out"), depth + 1, false, visited, diagnostics, recordRejection);
259
+ const innerType = getProperty(def, "innerType");
260
+ if (isObject(innerType)) screenPreConversionWalk(innerType, parentPointer, depth + 1, false, visited, diagnostics, recordRejection);
261
+ const valueType = getProperty(def, "valueType");
262
+ if (isObject(valueType)) screenPreConversionWalk(valueType, appendPointer(parentPointer, "additionalProperties"), depth + 1, false, visited, diagnostics, recordRejection);
263
+ const inner = safeCallNoArgs(getProperty(def, "getter"));
264
+ if (isObject(inner)) screenPreConversionWalk(inner, parentPointer, depth + 1, false, visited, diagnostics, recordRejection);
183
265
  }
184
266
  /**
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.
267
+ * Format an empty pointer as `<root>` so error messages do not contain
268
+ * a stray bare `""`. Non-empty pointers are returned verbatim.
189
269
  */
190
- function isCodecSchema(input) {
191
- const zod = getProperty(input, "_zod");
192
- if (!isObject(zod)) return false;
270
+ function formatPointer(pointer) {
271
+ return pointer === "" ? "<root>" : pointer;
272
+ }
273
+ /**
274
+ * True when a Zod node's `_zod.traits` set contains the named marker.
275
+ * Returns false when traits is absent or not a Set — Zod always
276
+ * populates it on real schemas, so the missing-Set case is treated as
277
+ * "marker not present".
278
+ */
279
+ function hasTrait(zod, traitName) {
193
280
  const traits = zod.traits;
194
- if (traits instanceof Set) return traits.has("$ZodCodec");
281
+ if (traits instanceof Set) return traits.has(traitName);
195
282
  return false;
196
283
  }
197
284
  /**
285
+ * Type guard narrowing `unknown` to a zero-argument function returning
286
+ * `unknown`. The narrowing is genuinely structural: `typeof === "function"`
287
+ * at runtime is exactly the membership test we want, and Zod has no
288
+ * way to make a getter "have the wrong arity" without breaking its own
289
+ * lazy implementation. Surfacing the narrowing through a guard means
290
+ * the call site can invoke `fn()` without an `as` assertion and the
291
+ * boundary lives in one named, documented location.
292
+ */
293
+ function isNoArgFunction(value) {
294
+ return typeof value === "function";
295
+ }
296
+ /**
297
+ * Invoke a value as a zero-argument function safely, returning whatever
298
+ * the function returns or `undefined` if it throws or is not callable.
299
+ * Centralises the lazy-schema getter invocation that both
300
+ * {@link containsNestedZod3Inner} and {@link screenPreConversionDescend}
301
+ * need; the throw is swallowed because the absence of a materialisable
302
+ * inner is not a screening concern — downstream `z.toJSONSchema` will
303
+ * surface any genuine construction failure with its own message.
304
+ */
305
+ function safeCallNoArgs(candidate) {
306
+ if (!isNoArgFunction(candidate)) return void 0;
307
+ try {
308
+ return candidate();
309
+ } catch {
310
+ return;
311
+ }
312
+ }
313
+ /**
198
314
  * Escape a string for inclusion in a `RegExp`. Required because Zod
199
315
  * messages contain `[`, `]`, `.`, `(`, and `)` characters which have regex
200
316
  * meaning. The set covers every character with special meaning in a
@@ -336,8 +452,11 @@ const CLASSIFIER_RULES = [
336
452
  prefix: "[toJSONSchema]: Non-representable type encountered:",
337
453
  kind: "zod-type-unrepresentable",
338
454
  build: (match, cause, schema, full) => {
339
- const trailing = match[1]?.trim() ?? "";
340
- const typeName = trailing.length > 0 ? trailing.split(/\s+/)[0] : void 0;
455
+ const trailing = match[1];
456
+ if (trailing === void 0) return describeUnparsableZodWording("Non-representable type prefix matched but no trailing capture", full, schema, cause);
457
+ const trimmed = trailing.trim();
458
+ const firstToken = trimmed.length > 0 ? trimmed.split(/\s+/)[0] : void 0;
459
+ const typeName = firstToken !== void 0 && firstToken.length > 0 ? firstToken : void 0;
341
460
  return new SchemaNormalisationError(`Zod encountered a schema kind${typeName !== void 0 ? ` "${typeName}"` : ""} with no JSON Schema processor registered. This usually means Zod added a new schema type that schema-components does not yet support. Original message: ${full}`, schema, "zod-type-unrepresentable", typeName, cause);
342
461
  }
343
462
  },
@@ -345,16 +464,22 @@ const CLASSIFIER_RULES = [
345
464
  prefix: "Cycle detected: ",
346
465
  kind: "zod-cycle-detected",
347
466
  build: (match, cause, schema, full) => {
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);
467
+ const trailing = match[1];
468
+ if (trailing === void 0) return describeUnparsableZodWording("Cycle detected prefix matched but no trailing capture", full, schema, cause);
469
+ const path = trailing.split(/\s+/)[0];
470
+ if (path === void 0 || path.length === 0) return describeUnparsableZodWording("Cycle detected message contained no pointer token", full, schema, cause);
471
+ return new SchemaNormalisationError(`Zod detected a cycle in the schema graph at ${path}. 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);
349
472
  }
350
473
  },
351
474
  {
352
475
  prefix: "Duplicate schema id \"",
353
476
  kind: "zod-duplicate-id",
354
477
  build: (match, cause, schema, full) => {
355
- const trailing = match[1] ?? "";
478
+ const trailing = match[1];
479
+ if (trailing === void 0) return describeUnparsableZodWording("Duplicate schema id prefix matched but no trailing capture", full, schema, cause);
356
480
  const closing = trailing.indexOf("\"");
357
- return new SchemaNormalisationError(`Two different Zod schemas share the same id "${closing === -1 ? trailing : trailing.slice(0, closing)}". JSON Schema requires distinct ids when multiple schemas are bundled together. Give each schema its own .meta({ id: ... }) or remove the duplicate. Original message: ${full}`, schema, "zod-duplicate-id", void 0, cause);
481
+ if (closing === -1) return describeUnparsableZodWording("Duplicate schema id message had no closing quote", full, schema, cause);
482
+ return new SchemaNormalisationError(`Two different Zod schemas share the same id "${trailing.slice(0, closing)}". JSON Schema requires distinct ids when multiple schemas are bundled together. Give each schema its own .meta({ id: ... }) or remove the duplicate. Original message: ${full}`, schema, "zod-duplicate-id", void 0, cause);
358
483
  }
359
484
  },
360
485
  {
@@ -377,6 +502,22 @@ const COMPILED_CLASSIFIER_RULES = CLASSIFIER_RULES.map((rule) => ({
377
502
  pattern: anchored(rule.prefix)
378
503
  }));
379
504
  /**
505
+ * Build a structured `zod-conversion-failed` error for the case where a
506
+ * classifier rule's prefix matched but the trailing capture or follow-on
507
+ * parsing could not extract the expected payload (cycle pointer,
508
+ * duplicate id, non-representable type name, ...).
509
+ *
510
+ * This replaces the previous pattern of substituting an empty string
511
+ * fallback — `match[1] ?? ""` would silently produce error messages like
512
+ * `"Zod detected a cycle in the schema graph at ."` whenever Zod's
513
+ * wording drifted, hiding the regression behind a misleading message.
514
+ * Raising a wording-regression error instead surfaces the drift loudly
515
+ * so the classifier rule (and its contract test) can be repaired.
516
+ */
517
+ function describeUnparsableZodWording(reason, fullMessage, schema, cause) {
518
+ return new SchemaNormalisationError(`Zod error matched a classifier prefix but the trailing message could not be parsed (${reason}). This usually means Zod has reworded the error since the classifier was last updated — the matching rule in adapter.ts CLASSIFIER_RULES needs to be revised to track the new wording. Original message: ${fullMessage}`, schema, "zod-conversion-failed", void 0, cause);
519
+ }
520
+ /**
380
521
  * Maximum recursion depth for {@link containsNestedZod3}. Reuses the
381
522
  * shared {@link MAX_REF_DEPTH} so the runtime walk and the compile-time
382
523
  * `DEFAULT_MAX_DEPTH` (type-aliased to the same value) stay in lockstep.
@@ -426,7 +567,13 @@ function containsNestedZod3Inner(value, visited, depth) {
426
567
  const def = value._def;
427
568
  const zod = value._zod;
428
569
  if (zod === void 0 && isObject(def)) return true;
429
- if (isObject(zod) && isObject(zod.def)) return containsNestedZod3Inner(zod.def, visited, depth + 1);
570
+ if (isObject(zod) && isObject(zod.def)) {
571
+ const def4 = zod.def;
572
+ if (def4.type === "lazy") {
573
+ if (containsNestedZod3Inner(safeCallNoArgs(def4.getter), visited, depth + 1)) return true;
574
+ }
575
+ return containsNestedZod3Inner(def4, visited, depth + 1);
576
+ }
430
577
  for (const key of Object.keys(value)) if (containsNestedZod3Inner(value[key], visited, depth + 1)) return true;
431
578
  return false;
432
579
  }
@@ -460,7 +607,10 @@ function normaliseSchema(input, ref, options) {
460
607
  case "zod3":
461
608
  result = normaliseZod3(input);
462
609
  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");
610
+ case "unsupported-schema-lib": {
611
+ const vendor = extractStandardSchemaVendor(input);
612
+ throw new SchemaNormalisationError(`Input looks like a schema from a non-Zod library — ${vendor !== void 0 ? `it self-identifies as the Standard Schema implementation "${vendor}"` : "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");
613
+ }
464
614
  case "openapi":
465
615
  if (!isObject(input)) throw new SchemaNormalisationError("Invalid OpenAPI document", input, "openapi-invalid");
466
616
  result = normaliseOpenApi(input, ref, options);
@@ -476,9 +626,8 @@ function normaliseSchema(input, ref, options) {
476
626
  function normaliseZod4(input, diagnostics) {
477
627
  const zod = getProperty(input, "_zod");
478
628
  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);
629
+ if (!isObject(getProperty(zod, "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");
630
+ screenPreConversion(input, diagnostics);
482
631
  const jsonSchema = callToJsonSchema(input);
483
632
  if (!isObject(jsonSchema)) throw new SchemaNormalisationError("z.toJSONSchema() did not produce an object", input, "invalid-zod");
484
633
  return {
@@ -598,7 +747,7 @@ function resolveOpenApiRef(doc, ref) {
598
747
  }
599
748
  if (ref.startsWith("#/")) {
600
749
  const resolved = dereference(ref, doc);
601
- if (resolved !== void 0) return resolved;
750
+ if (resolved !== void 0 && typeof resolved !== "boolean") return resolved;
602
751
  }
603
752
  throw new Error(`Unsupported OpenAPI ref format: ${ref}`);
604
753
  }
@@ -631,4 +780,4 @@ function extractRootMetaFromJson(jsonSchema) {
631
780
  return Object.keys(meta).length > 0 ? meta : void 0;
632
781
  }
633
782
  //#endregion
634
- export { __CLASSIFIER_RULES_FOR_TEST, detectSchemaKind, normaliseSchema };
783
+ export { __CLASSIFIER_RULES_FOR_TEST, detectSchemaKind, extractRootMetaFromJson, normaliseSchema };
@@ -1,5 +1,5 @@
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";
1
+ import { E as StringConstraints, f as FileConstraints, t as ArrayConstraints, x as ObjectConstraints, y as NumberConstraints } from "../types-BTB73MB8.mjs";
2
+ import { i as DiagnosticsOptions } from "../diagnostics-BS2kaUyE.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,4 +1,3 @@
1
- import { isObject } from "./guards.mjs";
2
1
  import { emitDiagnostic } from "./diagnostics.mjs";
3
2
  import { FORMAT_PATTERNS } from "./formats.mjs";
4
3
  //#region src/core/constraints.ts
@@ -10,10 +9,6 @@ function getNumber(obj, key) {
10
9
  const value = obj[key];
11
10
  return typeof value === "number" ? value : void 0;
12
11
  }
13
- function getObject(obj, key) {
14
- const value = obj[key];
15
- return isObject(value) ? value : void 0;
16
- }
17
12
  function extractStringConstraints(schema, diagnostics, pointer = "") {
18
13
  const c = {};
19
14
  const minLength = getNumber(schema, "minLength");
@@ -65,8 +60,6 @@ function extractArrayConstraints(schema) {
65
60
  if (minContains !== void 0) c.minContains = minContains;
66
61
  const maxContains = getNumber(schema, "maxContains");
67
62
  if (maxContains !== void 0) c.maxContains = maxContains;
68
- const unevaluatedItems = getObject(schema, "unevaluatedItems");
69
- if (unevaluatedItems !== void 0) c.unevaluatedItems = unevaluatedItems;
70
63
  return c;
71
64
  }
72
65
  function extractObjectConstraints(schema) {