schema-components 1.18.1 → 1.20.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 (68) hide show
  1. package/dist/core/adapter.d.mts +2 -2
  2. package/dist/core/adapter.mjs +128 -15
  3. package/dist/core/constraints.d.mts +2 -2
  4. package/dist/core/diagnostics.d.mts +1 -1
  5. package/dist/core/errors.d.mts +1 -1
  6. package/dist/core/errors.mjs +15 -1
  7. package/dist/core/fieldOrder.d.mts +1 -1
  8. package/dist/core/formats.d.mts +21 -14
  9. package/dist/core/formats.mjs +96 -4
  10. package/dist/core/merge.d.mts +1 -1
  11. package/dist/core/normalise.d.mts +38 -5
  12. package/dist/core/normalise.mjs +2 -2
  13. package/dist/core/openapi30.d.mts +33 -4
  14. package/dist/core/openapi30.mjs +2 -2
  15. package/dist/core/ref.d.mts +1 -1
  16. package/dist/core/renderer.d.mts +1 -1
  17. package/dist/core/renderer.mjs +7 -2
  18. package/dist/core/swagger2.d.mts +1 -1
  19. package/dist/core/swagger2.mjs +1 -1
  20. package/dist/core/typeInference.d.mts +2 -2
  21. package/dist/core/types.d.mts +1 -1
  22. package/dist/core/uri.d.mts +41 -0
  23. package/dist/core/uri.mjs +76 -0
  24. package/dist/core/version.d.mts +2 -2
  25. package/dist/core/version.mjs +43 -9
  26. package/dist/core/walkBuilders.d.mts +3 -3
  27. package/dist/core/walker.d.mts +1 -1
  28. package/dist/core/walker.mjs +50 -3
  29. package/dist/{diagnostics-BYk63jsC.d.mts → diagnostics-CbBPsxSt.d.mts} +1 -1
  30. package/dist/{errors-C5zRC2PU.d.mts → errors-C2iABcn9.d.mts} +14 -2
  31. package/dist/html/a11y.d.mts +2 -2
  32. package/dist/html/renderToHtml.d.mts +2 -2
  33. package/dist/html/renderToHtmlStream.d.mts +2 -2
  34. package/dist/html/renderers.d.mts +2 -2
  35. package/dist/html/renderers.mjs +37 -2
  36. package/dist/html/streamRenderers.d.mts +2 -2
  37. package/dist/normalise-CMMEl4cd.mjs +1306 -0
  38. package/dist/openapi/ApiCallbacks.d.mts +1 -1
  39. package/dist/openapi/ApiLinks.d.mts +1 -1
  40. package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
  41. package/dist/openapi/ApiSecurity.d.mts +1 -1
  42. package/dist/openapi/bundle.mjs +2 -0
  43. package/dist/openapi/components.d.mts +2 -2
  44. package/dist/openapi/components.mjs +20 -5
  45. package/dist/openapi/parser.d.mts +1 -1
  46. package/dist/openapi/parser.mjs +6 -1
  47. package/dist/openapi/resolve.d.mts +17 -6
  48. package/dist/openapi/resolve.mjs +45 -7
  49. package/dist/react/SchemaComponent.d.mts +21 -9
  50. package/dist/react/SchemaComponent.mjs +3 -13
  51. package/dist/react/SchemaView.d.mts +3 -3
  52. package/dist/react/SchemaView.mjs +1 -0
  53. package/dist/react/fieldPath.d.mts +1 -1
  54. package/dist/react/headless.d.mts +7 -1
  55. package/dist/react/headless.mjs +13 -1
  56. package/dist/react/headlessRenderers.d.mts +54 -3
  57. package/dist/react/headlessRenderers.mjs +153 -3
  58. package/dist/{ref-Ckt5liZs.d.mts → ref-C8JbwfiS.d.mts} +1 -1
  59. package/dist/{renderer-BAGoX4AK.d.mts → renderer-SOIbJBtk.d.mts} +9 -3
  60. package/dist/themes/mantine.d.mts +1 -1
  61. package/dist/themes/mui.d.mts +1 -1
  62. package/dist/themes/radix.d.mts +1 -1
  63. package/dist/themes/shadcn.d.mts +1 -1
  64. package/dist/{typeInference-5JiqIZ8t.d.mts → typeInference-CDoD_LZ_.d.mts} +187 -42
  65. package/dist/{types-D_5ST7SS.d.mts → types-C9zw9wbX.d.mts} +6 -0
  66. package/dist/{version-B5NV-35j.d.mts → version-D-u7aMfy.d.mts} +43 -1
  67. package/package.json +1 -1
  68. package/dist/normalise-tL9FckAk.mjs +0 -748
@@ -1,5 +1,6 @@
1
1
  import { isObject } from "../core/guards.mjs";
2
2
  import { sortFieldsByOrder } from "../core/fieldOrder.mjs";
3
+ import { isSafeHyperlink, isSafeMailtoAddress } from "../core/uri.mjs";
3
4
  import { jsx, jsxs } from "react/jsx-runtime";
4
5
  import { isValidElement, useCallback, useEffect, useRef } from "react";
5
6
  //#region src/react/headlessRenderers.tsx
@@ -83,13 +84,13 @@ function renderString(props) {
83
84
  children: "—"
84
85
  });
85
86
  const format = props.constraints.format;
86
- if (format === "email") return /* @__PURE__ */ jsx("a", {
87
+ if (format === "email" && isSafeMailtoAddress(strValue)) return /* @__PURE__ */ jsx("a", {
87
88
  href: `mailto:${strValue}`,
88
89
  id,
89
90
  "aria-readonly": "true",
90
91
  children: strValue
91
92
  });
92
- if (format === "uri" || format === "url") return /* @__PURE__ */ jsx("a", {
93
+ if ((format === "uri" || format === "url") && isSafeHyperlink(strValue)) return /* @__PURE__ */ jsx("a", {
93
94
  href: strValue,
94
95
  id,
95
96
  "aria-readonly": "true",
@@ -595,6 +596,155 @@ function renderRecursive(props) {
595
596
  " (recursive)"
596
597
  ] }) });
597
598
  }
599
+ /**
600
+ * Render a literal field — `z.literal("a")` or `{ const: 5 }`.
601
+ *
602
+ * Literals are non-editable by nature (the value is fixed at the schema
603
+ * level), so both read-only and editable modes display the literal value(s).
604
+ * Multiple literals (`z.literal(["a", "b"])`) render comma-separated.
605
+ */
606
+ function renderLiteral(props) {
607
+ const id = inputId(props.path);
608
+ if (props.tree.type !== "literal") return null;
609
+ const values = props.tree.literalValues;
610
+ if (values.length === 0) return /* @__PURE__ */ jsx("span", {
611
+ id,
612
+ "aria-readonly": "true",
613
+ children: "—"
614
+ });
615
+ return /* @__PURE__ */ jsx("span", {
616
+ id,
617
+ "aria-readonly": "true",
618
+ children: values.map((v) => v === null ? "null" : String(v)).join(", ")
619
+ });
620
+ }
621
+ /**
622
+ * Render a null field — `z.null()` or `{ type: "null" }`.
623
+ *
624
+ * The only valid value is `null`, so render an em-dash placeholder
625
+ * regardless of mode. There is nothing the user can usefully change.
626
+ */
627
+ function renderNull(props) {
628
+ return /* @__PURE__ */ jsx("span", {
629
+ id: inputId(props.path),
630
+ "aria-readonly": "true",
631
+ children: "—"
632
+ });
633
+ }
634
+ /**
635
+ * Render a never field — `z.never()` or `{ not: {} }` / `false` schema.
636
+ *
637
+ * `never` indicates a position that cannot hold any value. We render a
638
+ * visible placeholder rather than throwing because some valid schemas
639
+ * intentionally contain `never` branches (e.g. exhaustive discriminated
640
+ * unions), and a runtime crash on render would be worse than a visible
641
+ * indicator.
642
+ */
643
+ function renderNever(props) {
644
+ return /* @__PURE__ */ jsx("span", {
645
+ id: inputId(props.path),
646
+ "aria-readonly": "true",
647
+ className: "sc-never",
648
+ children: /* @__PURE__ */ jsx("em", { children: "never matches" })
649
+ });
650
+ }
651
+ /**
652
+ * Render a tuple field — `z.tuple([z.string(), z.number()])` or
653
+ * `{ prefixItems: [...] }`.
654
+ *
655
+ * Positional rendering: each `prefixItems` entry is rendered at its index.
656
+ * The structural index (e.g. `[0]`) is passed as the path suffix so
657
+ * children get unique ids and labels.
658
+ */
659
+ function renderTuple(props) {
660
+ if (props.tree.type !== "tuple") return null;
661
+ const prefixItems = props.tree.prefixItems;
662
+ const restItems = props.tree.restItems;
663
+ const arr = Array.isArray(props.value) ? props.value : [];
664
+ if (prefixItems.length === 0 && restItems === void 0 && arr.length === 0) return null;
665
+ const restCount = restItems !== void 0 ? Math.max(arr.length - prefixItems.length, 0) : 0;
666
+ return /* @__PURE__ */ jsxs("div", {
667
+ role: "group",
668
+ "aria-label": props.meta.description ?? void 0,
669
+ children: [prefixItems.map((element, i) => {
670
+ const itemValue = arr[i];
671
+ const childOnChange = (v) => {
672
+ const next = arr.slice();
673
+ next[i] = v;
674
+ props.onChange(next);
675
+ };
676
+ return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(element, itemValue, childOnChange, `[${String(i)}]`)) }, String(i));
677
+ }), restItems !== void 0 && Array.from({ length: restCount }, (_, j) => {
678
+ const i = prefixItems.length + j;
679
+ const itemValue = arr[i];
680
+ const childOnChange = (v) => {
681
+ const next = arr.slice();
682
+ next[i] = v;
683
+ props.onChange(next);
684
+ };
685
+ return /* @__PURE__ */ jsx("div", { children: toReactNode(props.renderChild(restItems, itemValue, childOnChange, `[${String(i)}]`)) }, `rest-${String(i)}`);
686
+ })]
687
+ });
688
+ }
689
+ /**
690
+ * Render a conditional field — JSON Schema `if`/`then`/`else`.
691
+ *
692
+ * Conditional schemas describe constraints rather than a single value
693
+ * shape, so the renderer surfaces each clause as a labelled fieldset.
694
+ * This mirrors the HTML renderer's annotation approach and gives a
695
+ * predictable structure for theme adapters that want to override it.
696
+ */
697
+ function renderConditional(props) {
698
+ if (props.tree.type !== "conditional") return null;
699
+ const { ifClause, thenClause, elseClause } = props.tree;
700
+ return /* @__PURE__ */ jsxs("fieldset", {
701
+ className: "sc-conditional",
702
+ children: [
703
+ /* @__PURE__ */ jsxs("div", {
704
+ className: "sc-conditional-if",
705
+ children: [
706
+ /* @__PURE__ */ jsx("strong", { children: "if:" }),
707
+ " ",
708
+ toReactNode(props.renderChild(ifClause, props.value, props.onChange))
709
+ ]
710
+ }),
711
+ thenClause !== void 0 && /* @__PURE__ */ jsxs("div", {
712
+ className: "sc-conditional-then",
713
+ children: [
714
+ /* @__PURE__ */ jsx("strong", { children: "then:" }),
715
+ " ",
716
+ toReactNode(props.renderChild(thenClause, props.value, props.onChange))
717
+ ]
718
+ }),
719
+ elseClause !== void 0 && /* @__PURE__ */ jsxs("div", {
720
+ className: "sc-conditional-else",
721
+ children: [
722
+ /* @__PURE__ */ jsx("strong", { children: "else:" }),
723
+ " ",
724
+ toReactNode(props.renderChild(elseClause, props.value, props.onChange))
725
+ ]
726
+ })
727
+ ]
728
+ });
729
+ }
730
+ /**
731
+ * Render a negation field — JSON Schema `{ not: { ... } }`.
732
+ *
733
+ * Negation describes a constraint ("value must NOT match this schema")
734
+ * rather than a value shape. The renderer surfaces the negated schema
735
+ * beneath an explanatory preamble.
736
+ */
737
+ function renderNegation(props) {
738
+ if (props.tree.type !== "negation") return null;
739
+ return /* @__PURE__ */ jsxs("fieldset", {
740
+ className: "sc-negation",
741
+ children: [
742
+ /* @__PURE__ */ jsx("strong", { children: "Must NOT match:" }),
743
+ " ",
744
+ toReactNode(props.renderChild(props.tree.negated, props.value, props.onChange))
745
+ ]
746
+ });
747
+ }
598
748
  function renderUnknown(props) {
599
749
  const id = inputId(props.path);
600
750
  if (props.readOnly) {
@@ -627,4 +777,4 @@ function matchUnionOption(options, value) {
627
777
  if (typeof value === "object" && value !== null) return options.find((o) => o.type === "object");
628
778
  }
629
779
  //#endregion
630
- export { defaultRecordValue, discriminatedUnionValueForTab, inputId, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderDiscriminatedUnion, renderEnum, renderFile, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderUnion, renderUnknown, toReactNode };
780
+ export { defaultRecordValue, discriminatedUnionValueForTab, inputId, nextRecordKey, renameRecordKey, renderArray, renderBoolean, renderConditional, renderDiscriminatedUnion, renderEnum, renderFile, renderLiteral, renderNegation, renderNever, renderNull, renderNumber, renderObject, renderRecord, renderRecursive, renderString, renderTuple, renderUnion, renderUnknown, toReactNode };
@@ -1,4 +1,4 @@
1
- import { i as DiagnosticsOptions } from "./diagnostics-BYk63jsC.mjs";
1
+ import { i as DiagnosticsOptions } from "./diagnostics-CbBPsxSt.mjs";
2
2
 
3
3
  //#region src/core/ref.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { D as StringConstraints, M as WalkedField, T as SchemaMeta, f as FileConstraints, t as ArrayConstraints, x as ObjectConstraints, y as NumberConstraints } from "./types-D_5ST7SS.mjs";
1
+ import { D as StringConstraints, M as WalkedField, T as SchemaMeta, f as FileConstraints, t as ArrayConstraints, x as ObjectConstraints, y as NumberConstraints } from "./types-C9zw9wbX.mjs";
2
2
 
3
3
  //#region src/core/renderer.d.ts
4
4
  /**
@@ -98,6 +98,7 @@ interface ComponentResolver {
98
98
  string?: RenderFunction;
99
99
  number?: RenderFunction;
100
100
  boolean?: RenderFunction;
101
+ null?: RenderFunction;
101
102
  enum?: RenderFunction;
102
103
  object?: RenderFunction;
103
104
  array?: RenderFunction;
@@ -110,6 +111,7 @@ interface ComponentResolver {
110
111
  recursive?: RenderFunction;
111
112
  literal?: RenderFunction;
112
113
  file?: RenderFunction;
114
+ never?: RenderFunction;
113
115
  unknown?: RenderFunction;
114
116
  }
115
117
  /** An HTML render function returns a string. */
@@ -122,6 +124,7 @@ interface HtmlResolver {
122
124
  string?: HtmlRenderFunction;
123
125
  number?: HtmlRenderFunction;
124
126
  boolean?: HtmlRenderFunction;
127
+ null?: HtmlRenderFunction;
125
128
  enum?: HtmlRenderFunction;
126
129
  object?: HtmlRenderFunction;
127
130
  array?: HtmlRenderFunction;
@@ -134,13 +137,16 @@ interface HtmlResolver {
134
137
  recursive?: HtmlRenderFunction;
135
138
  literal?: HtmlRenderFunction;
136
139
  file?: HtmlRenderFunction;
140
+ never?: HtmlRenderFunction;
137
141
  unknown?: HtmlRenderFunction;
138
142
  }
139
- declare const RESOLVER_KEYS: readonly ["string", "number", "boolean", "enum", "object", "array", "tuple", "record", "union", "discriminatedUnion", "conditional", "negation", "recursive", "literal", "file", "unknown"];
143
+ declare const RESOLVER_KEYS: readonly ["string", "number", "boolean", "null", "enum", "object", "array", "tuple", "record", "union", "discriminatedUnion", "conditional", "negation", "recursive", "literal", "file", "never", "unknown"];
140
144
  type ResolverKey = (typeof RESOLVER_KEYS)[number];
141
145
  /**
142
146
  * Map a schema type to the resolver key that handles it.
143
- * `discriminatedUnion` `union`. Unknown types `unknown`.
147
+ * Every WalkedField variant has a direct resolver key — exhaustive switch
148
+ * ensures new variants surface as a type error rather than silently
149
+ * falling through to "unknown".
144
150
  */
145
151
  declare function typeToKey(type: WalkedField["type"]): ResolverKey;
146
152
  /**
@@ -1,4 +1,4 @@
1
- import { r as ComponentResolver } from "../renderer-BAGoX4AK.mjs";
1
+ import { r as ComponentResolver } from "../renderer-SOIbJBtk.mjs";
2
2
 
3
3
  //#region src/themes/mantine.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { r as ComponentResolver } from "../renderer-BAGoX4AK.mjs";
1
+ import { r as ComponentResolver } from "../renderer-SOIbJBtk.mjs";
2
2
 
3
3
  //#region src/themes/mui.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { r as ComponentResolver } from "../renderer-BAGoX4AK.mjs";
1
+ import { r as ComponentResolver } from "../renderer-SOIbJBtk.mjs";
2
2
 
3
3
  //#region src/themes/radix.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { r as ComponentResolver } from "../renderer-BAGoX4AK.mjs";
1
+ import { r as ComponentResolver } from "../renderer-SOIbJBtk.mjs";
2
2
 
3
3
  //#region src/themes/shadcn.d.ts
4
4
  declare const shadcnResolver: ComponentResolver;
@@ -1,6 +1,43 @@
1
- import { d as FieldOverrides, u as FieldOverride } from "./types-D_5ST7SS.mjs";
1
+ import { d as FieldOverrides, u as FieldOverride } from "./types-C9zw9wbX.mjs";
2
+ import { z } from "zod";
2
3
 
3
4
  //#region src/core/typeInference.d.ts
5
+ /**
6
+ * Zod 4 types that `z.toJSONSchema()` rejects at runtime because they
7
+ * have no JSON Schema representation. The runtime adapter
8
+ * (`packages/core/src/core/adapter.ts` lines 106-116) catches the
9
+ * thrown error and surfaces it as a `SchemaNormalisationError` with
10
+ * kind `zod-type-unrepresentable` — but the failure only happens on
11
+ * first render. Statically rejecting these types at the props boundary
12
+ * gives the same diagnostic at compile time.
13
+ *
14
+ * SOURCE-OF-TRUTH: list mirrors `UNREPRESENTABLE_ZOD_TYPES` in
15
+ * `adapter.ts`. Add or remove entries here whenever the runtime list
16
+ * changes.
17
+ */
18
+ type UnrepresentableZodType = z.ZodBigInt | z.ZodDate | z.ZodMap | z.ZodSet | z.ZodSymbol | z.ZodFunction | z.ZodUndefined | z.ZodVoid | z.ZodNaN | z.ZodCodec;
19
+ /**
20
+ * Brand returned in place of a rejected Zod input. The descriptive
21
+ * literal is what TypeScript displays when the rejection fires, so
22
+ * developers see why their schema is incompatible.
23
+ */
24
+ interface UnrepresentableZodSchemaError {
25
+ readonly __schemaComponentsError: "Zod 4 type has no JSON Schema representation. See SchemaNormalisationError code 'zod-type-unrepresentable'.";
26
+ }
27
+ /**
28
+ * Reject Zod 4 inputs whose runtime conversion is known to throw.
29
+ *
30
+ * - When `T` is one of the {@link UnrepresentableZodType} variants, the
31
+ * resolved type is {@link UnrepresentableZodSchemaError}, which is
32
+ * not assignable from any legitimate Zod / JSON Schema / OpenAPI
33
+ * input — so the prop fails to typecheck.
34
+ * - Anything else (Zod 4 schemas that DO convert, JSON Schema literals,
35
+ * OpenAPI documents, `unknown` for runtime inputs) passes through
36
+ * unchanged.
37
+ *
38
+ * Wrapped in a tuple to suppress distribution over union types.
39
+ */
40
+ type RejectUnrepresentableZod<T> = [T] extends [UnrepresentableZodType] ? UnrepresentableZodSchemaError : T;
4
41
  /**
5
42
  * Convert a readonly tuple/array of values to a union type.
6
43
  * Handles both `as const` readonly tuples and mutable arrays.
@@ -30,6 +67,17 @@ type ArrayToUnion<A> = A extends readonly unknown[] ? A[number] : never;
30
67
  * - patternProperties -> merged into loose index signature
31
68
  */
32
69
  type FromJSONSchema<S, Defs extends Record<string, unknown> = Record<string, never>> = S extends {
70
+ nullable: true;
71
+ } ?
72
+ /**
73
+ * OpenAPI 3.0 `nullable: true` — surface the keyword wherever it
74
+ * appears (not just inside `ResolveMaybeRef`). The runtime path
75
+ * rewrites this to `anyOf: [T, { type: "null" }]` via
76
+ * `normaliseOpenApi30Node` (`openapi30.ts`). Mirroring at the
77
+ * `FromJSONSchema` level means nested fields inside refs preserve
78
+ * nullability when resolved.
79
+ */
80
+ FromJSONSchema<Omit<S, "nullable">, Defs> | null : S extends {
33
81
  $ref: infer R extends string;
34
82
  } ? ResolveSchemaRef<R, Defs> : S extends {
35
83
  $recursiveRef: string;
@@ -113,9 +161,16 @@ type DEFAULT_MAX_DEPTH = 10;
113
161
  * Supports:
114
162
  * - `#` (root)
115
163
  * - `#/$defs/Name` and `#/definitions/Name` (named definitions)
164
+ * - `#/components/schemas/Name` (OpenAPI 3.x component schemas)
116
165
  * - `#SomeName` ($anchor, $dynamicAnchor resolved from definitions)
166
+ *
167
+ * `#/components/schemas/` is resolved here for parity with the runtime's
168
+ * `dereference` (`ref.ts` line 217), which walks any `#/...` JSON Pointer
169
+ * uniformly. When the runtime walker encounters an inline `$ref` inside
170
+ * a Zod-converted or hand-written JSON Schema that points into the
171
+ * OpenAPI component tree, this branch produces the corresponding type.
117
172
  */
118
- type ResolveSchemaRef<R extends string, Defs extends Record<string, unknown>, Depth extends number = 0> = Depth extends DEFAULT_MAX_DEPTH ? __SchemaInferenceFellBack : R extends "#" ? unknown : R extends `#/$defs/${infer Name}` ? Name extends keyof Defs ? DetectRecursiveFallback<FromJSONSchema<Defs[Name], Defs>> : unknown : R extends `#/definitions/${infer Name}` ? Name extends keyof Defs ? DetectRecursiveFallback<FromJSONSchema<Defs[Name], Defs>> : unknown : R extends `#${infer AnchorName}` ? AnchorName extends keyof Defs ? DetectRecursiveFallback<FromJSONSchema<Defs[AnchorName], Defs>> : unknown : unknown;
173
+ type ResolveSchemaRef<R extends string, Defs extends Record<string, unknown>, Depth extends number = 0> = Depth extends DEFAULT_MAX_DEPTH ? __SchemaInferenceFellBack : R extends "#" ? unknown : R extends `#/$defs/${infer Name}` ? Name extends keyof Defs ? DetectRecursiveFallback<FromJSONSchema<Defs[Name], Defs>> : unknown : R extends `#/definitions/${infer Name}` ? Name extends keyof Defs ? DetectRecursiveFallback<FromJSONSchema<Defs[Name], Defs>> : unknown : R extends `#/components/schemas/${infer Name}` ? Name extends keyof Defs ? DetectRecursiveFallback<FromJSONSchema<Defs[Name], Defs>> : unknown : R extends `#${infer AnchorName}` ? AnchorName extends keyof Defs ? DetectRecursiveFallback<FromJSONSchema<Defs[AnchorName], Defs>> : unknown : unknown;
119
174
  /**
120
175
  * Merge an allOf array into an intersection type.
121
176
  */
@@ -178,12 +233,21 @@ type OmitArrayHelpers<S> = Omit<S, "prefixItems" | "items" | "additionalProperti
178
233
  /**
179
234
  * Parse an array schema: prefixItems -> tuple, items -> T[], or unknown[].
180
235
  *
236
+ * Draft 04 used tuple-form `items` (an array of schemas) for tuple typing;
237
+ * Draft 2020-12 renamed this to `prefixItems`. The runtime normaliser in
238
+ * `packages/core/src/core/normalise.ts` (lines 526-534) rewrites the legacy
239
+ * form to `prefixItems` before the walker sees it. We mirror that rewrite
240
+ * here so `as const` literals using the legacy form infer the same tuple
241
+ * type at compile time.
242
+ *
181
243
  * `contains` / `minContains` / `maxContains` constrain elements at runtime
182
244
  * but don't change the compile-time array element type.
183
245
  */
184
246
  type ArraySchemaToTs<S, Defs extends Record<string, unknown>> = S extends {
185
247
  prefixItems: infer P;
186
248
  } ? PrefixItemsToTuple<P, Defs> : S extends {
249
+ items: infer I extends readonly unknown[];
250
+ } ? PrefixItemsToTuple<I, Defs> : S extends {
187
251
  items: infer I;
188
252
  } ? FromJSONSchema<I, Defs>[] : unknown[];
189
253
  /**
@@ -243,14 +307,26 @@ type ExtractRawDefs<S> = S extends {
243
307
  } ? D extends Record<string, unknown> ? D : Record<string, never> : Record<string, never>;
244
308
  /**
245
309
  * Build a map of `$anchor` name -> schema from a definitions block.
246
- * Scans each definition value for `$anchor` or `$dynamicAnchor` and
247
- * creates entries like `{ Tree: <schema-with-$anchor-Tree> }`.
310
+ * Scans each definition value for `$anchor`, `$dynamicAnchor`, or the
311
+ * Draft 2019-09 `$recursiveAnchor` keyword and creates entries like
312
+ * `{ Tree: <schema-with-$anchor-Tree> }`.
313
+ *
314
+ * SOURCE-OF-TRUTH: mirrors `normaliseDraft201909NodeWithContext` in
315
+ * `packages/core/src/core/normalise.ts` (lines 638-650), which rewrites
316
+ * `$recursiveAnchor: true` to `$anchor: "__recursive__"` and a string
317
+ * `$recursiveAnchor: "name"` to `$anchor: "name"`. The corresponding
318
+ * `$recursiveRef: "#"` therefore resolves through the same `Defs` map
319
+ * as a modern `$ref: "#__recursive__"`.
248
320
  */
249
321
  type ExtractAnchors<D extends Record<string, unknown>> = { [K in keyof D as D[K] extends {
250
322
  $anchor: infer A extends string;
251
323
  } ? A : D[K] extends {
252
324
  $dynamicAnchor: infer A extends string;
253
- } ? A : never]: D[K] };
325
+ } ? A : D[K] extends {
326
+ $recursiveAnchor: infer A extends string;
327
+ } ? A : D[K] extends {
328
+ $recursiveAnchor: true;
329
+ } ? "__recursive__" : never]: D[K] };
254
330
  /**
255
331
  * Convert a union to an intersection.
256
332
  * `A | B` -> `A & B`. Used for allOf merging.
@@ -275,92 +351,161 @@ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) exten
275
351
  type ResolveOpenAPIRef<Spec extends Record<string, unknown>, Ref extends string> = Ref extends `#/components/schemas/${infer Name}` ? Spec["components"] extends Record<string, unknown> ? Spec["components"]["schemas"] extends Record<string, unknown> ? Name extends keyof Spec["components"]["schemas"] ? FromJSONSchema<Spec["components"]["schemas"][Name]> : unknown : unknown : unknown : Ref extends `#/definitions/${infer Name}` ? Spec["definitions"] extends Record<string, unknown> ? Name extends keyof Spec["definitions"] ? FromJSONSchema<Spec["definitions"][Name]> : unknown : unknown : Ref extends `#/paths/${infer PathRest}` ? ResolvePathBasedRef<Spec, PathRest> : unknown;
276
352
  /**
277
353
  * Resolve a path-based $ref after the `#/paths/` prefix.
278
- * Splits on `/` and navigates the document tree.
354
+ * Splits on `/` and navigates the document tree, decoding JSON Pointer
355
+ * tilde escapes (`~1` -> `/`, `~0` -> `~`) on every segment.
279
356
  *
280
- * Note: JSON Pointer tilde encoding (~1 for /, ~0 for ~) is not decoded
281
- * at the type level. For `as const` literals, use the literal path
282
- * character directly (e.g. `#/paths//pets/get/...`).
357
+ * SOURCE-OF-TRUTH: mirrors runtime `dereference` in
358
+ * `packages/core/src/core/ref.ts` (line 226), which applies the same
359
+ * `~1` -> `/`, `~0` -> `~` substitutions per RFC 6901 §4. The runtime
360
+ * uses ordered string replacement; the type-level mirror does the same
361
+ * via {@link DecodeJsonPointerSegment}.
283
362
  */
284
363
  type ResolvePathBasedRef<Spec extends Record<string, unknown>, PathRest extends string> = Spec["paths"] extends Record<string, unknown> ? ResolvePathSegments<Spec["paths"], SplitPath<PathRest>> : unknown;
364
+ /**
365
+ * Replace every occurrence of `From` with `To` inside `S`.
366
+ *
367
+ * Pure type-level alternative to `String.prototype.replaceAll` used for
368
+ * JSON Pointer escape decoding. Terminates when no further match is
369
+ * found in the tail.
370
+ */
371
+ type ReplaceAll<S extends string, From extends string, To extends string> = S extends `${infer Head}${From}${infer Tail}` ? `${Head}${To}${ReplaceAll<Tail, From, To>}` : S;
372
+ /**
373
+ * Decode a single JSON Pointer reference token per RFC 6901 §4:
374
+ * apply `~1` -> `/` first, then `~0` -> `~`. The order matters — an
375
+ * encoded `~` containing a literal `1` (e.g. `~01`) must remain `~1`
376
+ * after decoding, which only works when `~1` is processed first.
377
+ */
378
+ type DecodeJsonPointerSegment<S extends string> = ReplaceAll<ReplaceAll<S, "~1", "/">, "~0", "~">;
285
379
  /**
286
380
  * Split a path string on `/` into a tuple of segments.
287
381
  * The first segment is the path key (may be empty for `/pets` -> `""` / `"pets"`).
288
382
  */
289
383
  type SplitPath<S extends string> = S extends `${infer Head}/${infer Tail}` ? [Head, ...SplitPath<Tail>] : [S];
290
384
  /**
291
- * Recursively navigate into a document object by path segments.
385
+ * Recursively navigate into a document object by path segments. Each
386
+ * segment is JSON-Pointer-decoded before indexing so encoded forms such
387
+ * as `~1pets` correctly resolve to the `"/pets"` key.
292
388
  */
293
- type ResolvePathSegments<Doc, Segs extends string[]> = Segs extends [infer Head extends string, ...infer Rest extends string[]] ? Doc extends Record<string, unknown> ? Rest extends [] ? Doc[Head] : ResolvePathSegments<Doc[Head], Rest> : unknown : unknown;
389
+ type ResolvePathSegments<Doc, Segs extends string[]> = Segs extends [infer Head extends string, ...infer Rest extends string[]] ? Doc extends Record<string, unknown> ? DecodeJsonPointerSegment<Head> extends infer Decoded extends string ? Rest extends [] ? Doc[Decoded] : ResolvePathSegments<Doc[Decoded], Rest> : unknown : unknown : unknown;
294
390
  /** Navigate to a path item in an OpenAPI document. */
295
391
  type PathItemOf<Doc, Path extends string> = Doc extends {
296
392
  paths: Record<string, unknown>;
297
393
  } ? Path extends keyof Doc["paths"] ? Doc["paths"][Path] : unknown : unknown;
298
394
  /** Navigate to an operation within a path item. */
299
395
  type OperationOf<PathItem, Method extends string> = PathItem extends Record<string, unknown> ? Method extends keyof PathItem ? PathItem[Method] : unknown : unknown;
300
- /** Extract the schema from request body content (any media type). */
396
+ /**
397
+ * Extract the schema from request body content (any media type).
398
+ *
399
+ * `Record<string, { schema: infer S }>` already subsumes the previous
400
+ * `application/json`-specific branch — if the JSON content matches the
401
+ * specific shape it also matches the general index-signature pattern.
402
+ * The narrower branch was therefore unreachable and has been removed.
403
+ */
301
404
  type RequestBodySchemaOf<Op> = Op extends {
302
405
  requestBody: {
303
406
  content: Record<string, {
304
407
  schema: infer S;
305
408
  }>;
306
409
  };
307
- } ? S : Op extends {
308
- requestBody: {
309
- content: {
310
- "application/json": {
311
- schema: infer S;
312
- };
313
- };
314
- };
315
410
  } ? S : unknown;
316
- /** Extract the schema from response content (any media type). */
411
+ /**
412
+ * Extract the schema from response content (any media type).
413
+ *
414
+ * Same rationale as `RequestBodySchemaOf`: the index-signature branch
415
+ * subsumes the `application/json` branch, which was unreachable.
416
+ */
317
417
  type ResponseSchemaOf<Op, Status extends string> = Op extends {
318
418
  responses: Record<string, unknown>;
319
419
  } ? Status extends keyof Op["responses"] ? Op["responses"][Status] extends {
320
420
  content: Record<string, {
321
421
  schema: infer S;
322
422
  }>;
323
- } ? S : Op["responses"][Status] extends {
324
- content: {
325
- "application/json": {
326
- schema: infer S;
327
- };
328
- };
329
423
  } ? S : unknown : unknown : unknown;
330
- /** Resolve a schema that may be a $ref pointer. */
424
+ /**
425
+ * Resolve a schema that may be a `$ref` pointer.
426
+ *
427
+ * The `nullable: true` handling lives inside `FromJSONSchema` so it
428
+ * applies uniformly to direct schemas, refs, and nested fields. This
429
+ * helper only dispatches between ref-resolution and plain inference.
430
+ */
331
431
  type ResolveMaybeRef<Doc, S> = S extends {
332
432
  $ref: infer R extends string;
333
- } ? ResolveOpenAPIRef<Doc & Record<string, unknown>, R> : S extends {
334
- nullable: true;
335
- } & Record<string, unknown> ? FromJSONSchema<Omit<S, "nullable">> | null : S extends Record<string, unknown> ? FromJSONSchema<S> : unknown;
433
+ } ? ResolveOpenAPIRef<Doc & Record<string, unknown>, R> : S extends Record<string, unknown> ? FromJSONSchema<S> : unknown;
336
434
  /** Extract parameter names from an operation. */
337
435
  type ParameterNamesOf<Doc, Path extends string, Method extends string> = OperationOf<PathItemOf<Doc, Path>, Method> extends {
338
436
  parameters: readonly unknown[];
339
437
  } ? OperationOf<PathItemOf<Doc, Path>, Method>["parameters"][number] extends {
340
438
  name: infer N;
341
439
  } ? N extends string ? N : never : never : never;
440
+ /**
441
+ * Detect whether a document is Swagger 2.0 (OpenAPI 2.0).
442
+ *
443
+ * SOURCE-OF-TRUTH: mirrors runtime `isSwagger2` in
444
+ * `packages/core/src/core/version.ts`, which checks for `swagger: "2.0"`.
445
+ * The runtime path also recognises top-level `definitions` / parameters in
446
+ * the body location, but `swagger: "2.0"` is the canonical marker.
447
+ *
448
+ * Type-level Swagger 2.0 documents cannot be fully normalised at compile
449
+ * time — the rewrite reorders the document tree (definitions →
450
+ * components/schemas, body parameters → requestBody, etc.) in ways
451
+ * TypeScript's mapped-type machinery cannot express. Detecting the
452
+ * version is tractable, so we surface `__SchemaInferenceFellBack`
453
+ * deliberately rather than silently producing `unknown`.
454
+ */
455
+ type IsSwagger2Doc<Doc> = Doc extends {
456
+ swagger: "2.0";
457
+ } ? true : false;
342
458
  /**
343
459
  * Infer the TypeScript type of an OpenAPI operation's request body.
460
+ *
461
+ * Swagger 2.0 documents are not normalised at the type level. When the
462
+ * input is Swagger 2.0, this returns `__SchemaInferenceFellBack` so
463
+ * callers can detect the fallback explicitly via a conditional type.
344
464
  */
345
- type OpenAPIRequestBodyType<Doc, Path extends string, Method extends string> = ResolveMaybeRef<Doc, RequestBodySchemaOf<OperationOf<PathItemOf<Doc, Path>, Method>>>;
465
+ type OpenAPIRequestBodyType<Doc, Path extends string, Method extends string> = IsSwagger2Doc<Doc> extends true ? __SchemaInferenceFellBack : ResolveMaybeRef<Doc, RequestBodySchemaOf<OperationOf<PathItemOf<Doc, Path>, Method>>>;
346
466
  /**
347
467
  * Infer the TypeScript type of an OpenAPI operation's response.
468
+ *
469
+ * Swagger 2.0 documents are not normalised at the type level. When the
470
+ * input is Swagger 2.0, this returns `__SchemaInferenceFellBack` so
471
+ * callers can detect the fallback explicitly via a conditional type.
472
+ */
473
+ type OpenAPIResponseType<Doc, Path extends string, Method extends string, Status extends string> = IsSwagger2Doc<Doc> extends true ? __SchemaInferenceFellBack : ResolveMaybeRef<Doc, ResponseSchemaOf<OperationOf<PathItemOf<Doc, Path>, Method>, Status>>;
474
+ /**
475
+ * Convert a resolved request/response type into the corresponding
476
+ * `fields` prop type used by ApiRequestBody / ApiResponse:
477
+ *
478
+ * - `__SchemaInferenceFellBack` (Swagger 2.0, depth-exceeded refs) is
479
+ * preserved verbatim so callers can detect the brand.
480
+ * - `unknown` (no schema found at the supplied path/status) falls back
481
+ * to the loose `Record<string, FieldOverride>` shape so runtime
482
+ * documents still typecheck.
483
+ * - Any other concrete type is mapped through `FieldOverrides`.
484
+ *
485
+ * The brand check intentionally precedes the `unknown` check. The brand
486
+ * is a structural object type and is therefore NOT assignable to
487
+ * `unknown extends T` — checking that first would always short-circuit
488
+ * to the loose `Record` fallback and the brand would never surface.
348
489
  */
349
- type OpenAPIResponseType<Doc, Path extends string, Method extends string, Status extends string> = ResolveMaybeRef<Doc, ResponseSchemaOf<OperationOf<PathItemOf<Doc, Path>, Method>, Status>>;
490
+ type FieldsFromInferred<T> = [T] extends [__SchemaInferenceFellBack] ? __SchemaInferenceFellBack : unknown extends T ? Record<string, FieldOverride> : FieldOverrides<T>;
350
491
  /**
351
492
  * Infer the fields prop type for ApiRequestBody.
352
- * Surfaces `__SchemaInferenceFellBack` when the schema contains
353
- * recursive $ref chains that exceed type-level depth limits.
354
- * Falls back to `Record<string, FieldOverride>` for runtime documents.
493
+ *
494
+ * Surfaces `__SchemaInferenceFellBack` for Swagger 2.0 documents and
495
+ * for schemas whose $ref chains exceed type-level depth limits. Falls
496
+ * back to `Record<string, FieldOverride>` for runtime documents whose
497
+ * shape cannot be inferred at compile time.
355
498
  */
356
- type InferRequestBodyFields<Doc, Path extends string, Method extends string> = unknown extends OpenAPIRequestBodyType<Doc, Path, Method> ? OpenAPIRequestBodyType<Doc, Path, Method> extends __SchemaInferenceFellBack ? __SchemaInferenceFellBack : Record<string, FieldOverride> : FieldOverrides<OpenAPIRequestBodyType<Doc, Path, Method>>;
499
+ type InferRequestBodyFields<Doc, Path extends string, Method extends string> = FieldsFromInferred<OpenAPIRequestBodyType<Doc, Path, Method>>;
357
500
  /**
358
501
  * Infer the fields prop type for ApiResponse.
359
- * Surfaces `__SchemaInferenceFellBack` when the schema contains
360
- * recursive $ref chains that exceed type-level depth limits.
361
- * Falls back to `Record<string, FieldOverride>` for runtime documents.
502
+ *
503
+ * Surfaces `__SchemaInferenceFellBack` for Swagger 2.0 documents and
504
+ * for schemas whose $ref chains exceed type-level depth limits. Falls
505
+ * back to `Record<string, FieldOverride>` for runtime documents whose
506
+ * shape cannot be inferred at compile time.
362
507
  */
363
- type InferResponseFields<Doc, Path extends string, Method extends string, Status extends string> = unknown extends OpenAPIResponseType<Doc, Path, Method, Status> ? OpenAPIResponseType<Doc, Path, Method, Status> extends __SchemaInferenceFellBack ? __SchemaInferenceFellBack : Record<string, FieldOverride> : FieldOverrides<OpenAPIResponseType<Doc, Path, Method, Status>>;
508
+ type InferResponseFields<Doc, Path extends string, Method extends string, Status extends string> = FieldsFromInferred<OpenAPIResponseType<Doc, Path, Method, Status>>;
364
509
  /**
365
510
  * Infer the overrides prop type for ApiParameters.
366
511
  * Falls back to `Record<string, FieldOverride>` for runtime documents.
@@ -385,4 +530,4 @@ type PathOfType<T, Prefix extends string = ""> = IsNarrowObject<T> extends true
385
530
  */
386
531
  type TypeAtPath<T, P extends string> = P extends `${infer Key}.${infer Rest}` ? Key extends keyof T ? TypeAtPath<T[Key], Rest> : unknown : P extends keyof T ? T[P] : unknown;
387
532
  //#endregion
388
- export { InferResponseFields as a, PathOfType as c, UnsafeFields as d, __SchemaInferenceFellBack as f, InferRequestBodyFields as i, ResolveOpenAPIRef as l, FromJSONSchema as n, OpenAPIRequestBodyType as o, InferParameterOverrides as r, OpenAPIResponseType as s, DEFAULT_MAX_DEPTH as t, TypeAtPath as u };
533
+ export { InferResponseFields as a, PathOfType as c, TypeAtPath as d, UnrepresentableZodSchemaError as f, __SchemaInferenceFellBack as h, InferRequestBodyFields as i, RejectUnrepresentableZod as l, UnsafeFields as m, FromJSONSchema as n, OpenAPIRequestBodyType as o, UnrepresentableZodType as p, InferParameterOverrides as r, OpenAPIResponseType as s, DEFAULT_MAX_DEPTH as t, ResolveOpenAPIRef as u };
@@ -184,6 +184,12 @@ interface TupleField extends FieldBase {
184
184
  constraints: ArrayConstraints;
185
185
  /** Positional element schemas from `prefixItems`. */
186
186
  prefixItems: WalkedField[];
187
+ /**
188
+ * Schema for items beyond the `prefixItems` length. In Draft 2020-12,
189
+ * `items` adjacent to `prefixItems` describes the rest element. When
190
+ * absent, additional items are permitted but unconstrained.
191
+ */
192
+ restItems?: WalkedField;
187
193
  }
188
194
  interface RecordField extends FieldBase {
189
195
  type: "record";