schema-components 1.20.0 → 1.21.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 (58) hide show
  1. package/dist/core/adapter.d.mts +9 -2
  2. package/dist/core/adapter.mjs +220 -64
  3. package/dist/core/constraints.d.mts +1 -1
  4. package/dist/core/constraints.mjs +0 -2
  5. package/dist/core/errors.d.mts +1 -1
  6. package/dist/core/errors.mjs +9 -15
  7. package/dist/core/fieldOrder.d.mts +1 -1
  8. package/dist/core/merge.d.mts +10 -1
  9. package/dist/core/merge.mjs +11 -0
  10. package/dist/core/normalise.d.mts +7 -1
  11. package/dist/core/normalise.mjs +1 -1
  12. package/dist/core/openapi30.d.mts +24 -1
  13. package/dist/core/openapi30.mjs +2 -2
  14. package/dist/core/ref.d.mts +1 -1
  15. package/dist/core/ref.mjs +34 -9
  16. package/dist/core/renderer.d.mts +1 -1
  17. package/dist/core/swagger2.mjs +1 -1
  18. package/dist/core/typeInference.d.mts +1 -1
  19. package/dist/core/types.d.mts +1 -1
  20. package/dist/core/walkBuilders.d.mts +12 -4
  21. package/dist/core/walkBuilders.mjs +11 -3
  22. package/dist/core/walker.d.mts +1 -1
  23. package/dist/core/walker.mjs +32 -25
  24. package/dist/{errors-C2iABcn9.d.mts → errors-QEwOtQAA.d.mts} +7 -11
  25. package/dist/html/a11y.d.mts +2 -2
  26. package/dist/html/renderToHtml.d.mts +2 -2
  27. package/dist/html/renderToHtmlStream.d.mts +2 -2
  28. package/dist/html/renderers.d.mts +2 -2
  29. package/dist/html/renderers.mjs +1 -1
  30. package/dist/html/streamRenderers.d.mts +2 -2
  31. package/dist/{normalise-CMMEl4cd.mjs → normalise-DaSrnr8g.mjs} +325 -28
  32. package/dist/openapi/ApiCallbacks.d.mts +1 -1
  33. package/dist/openapi/ApiLinks.d.mts +1 -1
  34. package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
  35. package/dist/openapi/ApiSecurity.d.mts +1 -1
  36. package/dist/openapi/ApiSecurity.mjs +113 -7
  37. package/dist/openapi/components.d.mts +32 -10
  38. package/dist/openapi/components.mjs +22 -12
  39. package/dist/openapi/parser.d.mts +1 -1
  40. package/dist/openapi/parser.mjs +39 -4
  41. package/dist/openapi/resolve.d.mts +60 -9
  42. package/dist/openapi/resolve.mjs +86 -23
  43. package/dist/react/SchemaComponent.d.mts +4 -4
  44. package/dist/react/SchemaComponent.mjs +32 -4
  45. package/dist/react/SchemaView.d.mts +2 -2
  46. package/dist/react/fieldPath.d.mts +1 -1
  47. package/dist/react/headless.d.mts +1 -1
  48. package/dist/react/headlessRenderers.d.mts +2 -2
  49. package/dist/react/headlessRenderers.mjs +1 -1
  50. package/dist/{ref-C8JbwfiS.d.mts → ref-si8ViYun.d.mts} +6 -1
  51. package/dist/{renderer-SOIbJBtk.d.mts → renderer-DI6ZYf7a.d.mts} +1 -1
  52. package/dist/themes/mantine.d.mts +1 -1
  53. package/dist/themes/mui.d.mts +1 -1
  54. package/dist/themes/radix.d.mts +1 -1
  55. package/dist/themes/shadcn.d.mts +1 -1
  56. package/dist/{typeInference-CDoD_LZ_.d.mts → typeInference-Bxw3NOG1.d.mts} +162 -48
  57. package/dist/{types-C9zw9wbX.d.mts → types-BnxPEElk.d.mts} +12 -2
  58. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { d as FieldOverrides, u as FieldOverride } from "./types-C9zw9wbX.mjs";
1
+ import { d as FieldOverrides, u as FieldOverride } from "./types-BnxPEElk.mjs";
2
2
  import { z } from "zod";
3
3
 
4
4
  //#region src/core/typeInference.d.ts
@@ -24,20 +24,51 @@ type UnrepresentableZodType = z.ZodBigInt | z.ZodDate | z.ZodMap | z.ZodSet | z.
24
24
  interface UnrepresentableZodSchemaError {
25
25
  readonly __schemaComponentsError: "Zod 4 type has no JSON Schema representation. See SchemaNormalisationError code 'zod-type-unrepresentable'.";
26
26
  }
27
+ /**
28
+ * Recursively unwrap Zod 4 wrappers that hold an inner schema —
29
+ * `ZodOptional`, `ZodNullable`, `ZodReadonly`, `ZodLazy`, and `ZodPipe`.
30
+ *
31
+ * The runtime conversion still throws for `z.optional(z.bigint())`,
32
+ * `z.lazy(() => z.bigint())`, `z.nullable(z.bigint())`,
33
+ * `z.bigint().readonly()`, and `z.bigint().pipe(z.bigint())` because
34
+ * `z.toJSONSchema()` walks into the wrapped schema before reporting the
35
+ * unrepresentable type. The compile-time rejection must therefore peel
36
+ * those wrappers off before checking against the rejection list, or the
37
+ * brand would never surface for wrapped inputs.
38
+ *
39
+ * `ZodCodec` is short-circuited at the top because it is itself listed
40
+ * in {@link UnrepresentableZodType}. It extends `ZodPipe` structurally,
41
+ * so without the early exit the `ZodPipe` branch would unwrap a bare
42
+ * codec into its two sides and the rejection would no longer fire for
43
+ * `z.codec(...)`.
44
+ *
45
+ * `ZodPipe` (non-codec) has two slots (`in` and `out`); both are
46
+ * unwrapped into a union so a single unrepresentable side surfaces via
47
+ * {@link AnyMemberIsUnrepresentable}.
48
+ */
49
+ type UnwrapZodWrapper<T> = T extends z.ZodCodec ? T : T extends z.ZodOptional<infer Inner> ? UnwrapZodWrapper<Inner> : T extends z.ZodNullable<infer Inner> ? UnwrapZodWrapper<Inner> : T extends z.ZodReadonly<infer Inner> ? UnwrapZodWrapper<Inner> : T extends z.ZodLazy<infer Inner> ? UnwrapZodWrapper<Inner> : T extends z.ZodPipe<infer InnerIn, infer InnerOut> ? UnwrapZodWrapper<InnerIn> | UnwrapZodWrapper<InnerOut> : T;
50
+ /**
51
+ * True when any member of the (possibly unioned) input extends one of
52
+ * the unrepresentable Zod types. Distribution over the union ensures a
53
+ * single unrepresentable member triggers true — matching runtime
54
+ * semantics for `ZodPipe`, whose two sides expand to a union via
55
+ * {@link UnwrapZodWrapper}.
56
+ */
57
+ type AnyMemberIsUnrepresentable<T> = (T extends UnrepresentableZodType ? true : false) extends false ? false : true;
27
58
  /**
28
59
  * Reject Zod 4 inputs whose runtime conversion is known to throw.
29
60
  *
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.
61
+ * - When `T` (or any inner schema wrapped by `ZodOptional`,
62
+ * `ZodNullable`, `ZodReadonly`, `ZodLazy`, or `ZodPipe`) is one of
63
+ * the {@link UnrepresentableZodType} variants, the resolved type is
64
+ * {@link UnrepresentableZodSchemaError}, which is not assignable from
65
+ * any legitimate Zod / JSON Schema / OpenAPI input — so the prop
66
+ * fails to typecheck.
34
67
  * - Anything else (Zod 4 schemas that DO convert, JSON Schema literals,
35
68
  * OpenAPI documents, `unknown` for runtime inputs) passes through
36
69
  * unchanged.
37
- *
38
- * Wrapped in a tuple to suppress distribution over union types.
39
70
  */
40
- type RejectUnrepresentableZod<T> = [T] extends [UnrepresentableZodType] ? UnrepresentableZodSchemaError : T;
71
+ type RejectUnrepresentableZod<T> = AnyMemberIsUnrepresentable<UnwrapZodWrapper<T>> extends true ? UnrepresentableZodSchemaError : T;
41
72
  /**
42
73
  * Convert a readonly tuple/array of values to a union type.
43
74
  * Handles both `as const` readonly tuples and mutable arrays.
@@ -66,7 +97,7 @@ type ArrayToUnion<A> = A extends readonly unknown[] ? A[number] : never;
66
97
  * - not -> unknown (negation not expressible in TS)
67
98
  * - patternProperties -> merged into loose index signature
68
99
  */
69
- type FromJSONSchema<S, Defs extends Record<string, unknown> = Record<string, never>> = S extends {
100
+ type FromJSONSchema<S, Defs extends Record<string, unknown> = Record<string, never>, Depth extends readonly unknown[] = []> = MergeRootDefs<S, Defs> extends infer MergedDefs extends Record<string, unknown> ? S extends {
70
101
  nullable: true;
71
102
  } ?
72
103
  /**
@@ -77,21 +108,21 @@ type FromJSONSchema<S, Defs extends Record<string, unknown> = Record<string, nev
77
108
  * `FromJSONSchema` level means nested fields inside refs preserve
78
109
  * nullability when resolved.
79
110
  */
80
- FromJSONSchema<Omit<S, "nullable">, Defs> | null : S extends {
111
+ FromJSONSchema<Omit<S, "nullable">, MergedDefs, Depth> | null : S extends {
81
112
  $ref: infer R extends string;
82
- } ? ResolveSchemaRef<R, Defs> : S extends {
113
+ } ? ResolveSchemaRef<R, MergedDefs, Depth> : S extends {
83
114
  $recursiveRef: string;
84
115
  } ? unknown : S extends {
85
116
  $dynamicRef: infer R extends string;
86
- } ? ResolveSchemaRef<R, Defs> : S extends {
117
+ } ? ResolveSchemaRef<R, MergedDefs, Depth> : S extends {
87
118
  allOf: infer A;
88
- } ? AllOfToType<A, Defs> : S extends {
119
+ } ? AllOfToType<A, MergedDefs, Depth> : S extends {
89
120
  anyOf: infer A;
90
- } ? UnionOfMembers<A, Defs> : S extends {
121
+ } ? UnionOfMembers<A, MergedDefs, Depth> : S extends {
91
122
  oneOf: infer A;
92
- } ? UnionOfMembers<A, Defs> : S extends {
123
+ } ? UnionOfMembers<A, MergedDefs, Depth> : S extends {
93
124
  if: unknown;
94
- } ? FromJSONSchema<Omit<S, "if" | "then" | "else">, Defs> : S extends {
125
+ } ? FromJSONSchema<Omit<S, "if" | "then" | "else">, MergedDefs, Depth> : S extends {
95
126
  not: unknown;
96
127
  } ? unknown : S extends {
97
128
  const: infer V;
@@ -99,7 +130,68 @@ FromJSONSchema<Omit<S, "nullable">, Defs> | null : S extends {
99
130
  enum: infer E;
100
131
  } ? ArrayToUnion<E> : S extends {
101
132
  type: infer T;
102
- } ? TypeToTs<T, S, Defs> : S extends readonly (infer E)[] ? E : unknown;
133
+ } ? TypeToTs<T, S, MergedDefs, Depth> : S extends readonly (infer E)[] ? E : unknown : unknown;
134
+ /**
135
+ * Merge `$defs` / `definitions` declared at the current schema position with
136
+ * the caller-supplied `Defs` map BEFORE the ref/allOf/anyOf/oneOf dispatch.
137
+ *
138
+ * SOURCE-OF-TRUTH: parity with the runtime walker, which uses `rootDocument`
139
+ * (see `packages/core/src/core/ref.ts` line 91) to resolve any `$ref` against
140
+ * the full document — including sibling definitions colocated with the
141
+ * reference. Without this merge, a legal schema like
142
+ * `{ $ref: "#/definitions/Foo", definitions: { Foo: {...} } }` would lose
143
+ * its sibling defs because the ref branch fires before `ExtractDefs` runs
144
+ * inside `ObjectSchemaToTs`.
145
+ *
146
+ * Merge semantics (per-key resolution via {@link CollisionSafeMerge}):
147
+ * - Parent-only keys: parent value wins
148
+ * - Local-only keys: local value wins
149
+ * - Shared keys: parent value wins (caller / inherited context takes
150
+ * precedence over a deeper redeclaration)
151
+ *
152
+ * When the current schema declares no local defs (`HasLocalDefs<S>` is
153
+ * `false`), `ParentDefs` is returned unchanged so the inherited context
154
+ * is never poisoned by the empty index-signature sentinel.
155
+ */
156
+ type MergeRootDefs<S, ParentDefs extends Record<string, unknown>> = HasLocalDefs<S> extends true ? ExtractRawDefs<S> extends infer RawDefs extends Record<string, unknown> ? CollisionSafeMerge<CollisionSafeMerge<RawDefs, ExtractAnchors<RawDefs>>, ParentDefs> : ParentDefs : ParentDefs;
157
+ /**
158
+ * Merge two record-shaped types where keys present in `B` always take
159
+ * precedence over the same key in `A`.
160
+ *
161
+ * The empty default `Record<string, never>` is detected explicitly via
162
+ * {@link IsEmptyDefs}: when either side is the sentinel, the other side
163
+ * is returned unchanged. This avoids two pitfalls of the naive
164
+ * `Omit<A, keyof B> & B` approach:
165
+ *
166
+ * 1. `keyof Record<string, never>` is the entire `string` type, so
167
+ * `Omit<A, string>` would strip every key from `A`.
168
+ * 2. Iterating a mapped type over `keyof A | keyof B` where either side
169
+ * contributes `string` collapses every entry to the index-signature
170
+ * value (`never` in the sentinel), wiping the literal keys from the
171
+ * other side.
172
+ *
173
+ * Only when both sides hold concrete literal keys does the per-key
174
+ * mapped merge run.
175
+ */
176
+ type CollisionSafeMerge<A, B> = IsEmptyDefs<A> extends true ? B : IsEmptyDefs<B> extends true ? A : { [K in keyof A | keyof B]: K extends keyof B ? B[K] : K extends keyof A ? A[K] : never };
177
+ /**
178
+ * True for the empty-default `Record<string, never>` sentinel used as the
179
+ * initial `Defs` map — i.e. an open index signature `[string]: never`
180
+ * with no literal keys. Distinguished from a record with at least one
181
+ * literal key by checking that `string extends keyof T`.
182
+ */
183
+ type IsEmptyDefs<T> = [keyof T] extends [never] ? true : string extends keyof T ? true : false;
184
+ /**
185
+ * True when the schema declares `$defs` or `definitions` as an object,
186
+ * false otherwise. Used by {@link MergeRootDefs} and {@link ExtractDefs}
187
+ * to avoid intersecting the parent context with an empty
188
+ * index-signature sentinel.
189
+ */
190
+ type HasLocalDefs<S> = S extends {
191
+ $defs: Record<string, unknown>;
192
+ } ? true : S extends {
193
+ definitions: Record<string, unknown>;
194
+ } ? true : false;
103
195
  /**
104
196
  * Marker type emitted when OpenAPI $ref resolution hits the type-level
105
197
  * recursion depth limit. Instead of silently falling back to
@@ -137,18 +229,23 @@ type DetectRecursiveFallback<T> = unknown extends T ? __SchemaInferenceFellBack
137
229
  *
138
230
  * The TypeScript type system imposes its own recursion limit; without an
139
231
  * explicit bound a cyclic schema graph would exhaust it and degrade to
140
- * `any`/`unknown` silently. Ten levels is the runtime walker's parallel
141
- * — see `countDistinctRefs` in `ref.ts` (lines 52-55), which derives its
142
- * bound from the number of distinct `$ref` strings in the document.
232
+ * `any`/`unknown` silently. This number is the runtime walker's parallel
233
+ * — see `resolveRef` in `packages/core/src/core/ref.ts` (line 119), whose
234
+ * default `maxDepth` is `64`. Matching the runtime bound here means a
235
+ * schema that the runtime resolves successfully is never silently dropped
236
+ * to `__SchemaInferenceFellBack` at compile time purely because the
237
+ * type-level limit was lower.
143
238
  *
144
239
  * A fixed bound is used here rather than a derived one because the type
145
240
  * system has no way to count distinct strings across a recursive `Defs`
146
241
  * map without itself recursing — which is the problem the bound exists
147
- * to solve. Ten covers every realistic schema graph encountered in
148
- * practice; deeper graphs surface as `__SchemaInferenceFellBack` so
149
- * consumers can detect the limit explicitly.
242
+ * to solve. Real-world OpenAPI documents (Stripe, GitHub, AWS) routinely
243
+ * contain 30-100+ distinct `$ref` strings, so a low ceiling would mask
244
+ * legitimate references. Deeper graphs surface as
245
+ * `__SchemaInferenceFellBack` so consumers can detect the limit
246
+ * explicitly.
150
247
  */
151
- type DEFAULT_MAX_DEPTH = 10;
248
+ type DEFAULT_MAX_DEPTH = 64;
152
249
  /**
153
250
  * Resolve a $ref against the local definitions context.
154
251
  *
@@ -170,11 +267,11 @@ type DEFAULT_MAX_DEPTH = 10;
170
267
  * a Zod-converted or hand-written JSON Schema that points into the
171
268
  * OpenAPI component tree, this branch produces the corresponding type.
172
269
  */
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;
270
+ type ResolveSchemaRef<R extends string, Defs extends Record<string, unknown>, Depth extends readonly unknown[] = []> = Depth["length"] extends DEFAULT_MAX_DEPTH ? __SchemaInferenceFellBack : R extends "#" ? unknown : R extends `#/$defs/${infer Name}` ? Name extends keyof Defs ? DetectRecursiveFallback<FromJSONSchema<Defs[Name], Defs, [unknown, ...Depth]>> : unknown : R extends `#/definitions/${infer Name}` ? Name extends keyof Defs ? DetectRecursiveFallback<FromJSONSchema<Defs[Name], Defs, [unknown, ...Depth]>> : unknown : R extends `#/components/schemas/${infer Name}` ? Name extends keyof Defs ? DetectRecursiveFallback<FromJSONSchema<Defs[Name], Defs, [unknown, ...Depth]>> : unknown : R extends `#${infer AnchorName}` ? AnchorName extends keyof Defs ? DetectRecursiveFallback<FromJSONSchema<Defs[AnchorName], Defs, [unknown, ...Depth]>> : unknown : unknown;
174
271
  /**
175
272
  * Merge an allOf array into an intersection type.
176
273
  */
177
- type AllOfToType<A, Defs extends Record<string, unknown>> = A extends readonly unknown[] ? UnionToIntersection<FromJSONSchema<A[number], Defs>> : unknown;
274
+ type AllOfToType<A, Defs extends Record<string, unknown>, Depth extends readonly unknown[] = []> = A extends readonly unknown[] ? UnionToIntersection<FromJSONSchema<A[number], Defs, Depth>> : unknown;
178
275
  /**
179
276
  * Convert an anyOf/oneOf array into a union type.
180
277
  *
@@ -195,7 +292,7 @@ type AllOfToType<A, Defs extends Record<string, unknown>> = A extends readonly u
195
292
  * nullable when at least one null member is present — mirrors the
196
293
  * walker's `normaliseAnyOf`.
197
294
  */
198
- type UnionOfMembers<A, Defs extends Record<string, unknown>> = A extends readonly unknown[] ? HasNullMember<A> extends true ? Exclude<FromJSONSchema<A[number], Defs>, null> | null : FromJSONSchema<A[number], Defs> : unknown;
295
+ type UnionOfMembers<A, Defs extends Record<string, unknown>, Depth extends readonly unknown[] = []> = A extends readonly unknown[] ? HasNullMember<A> extends true ? Exclude<FromJSONSchema<A[number], Defs, Depth>, null> | null : FromJSONSchema<A[number], Defs, Depth> : unknown;
199
296
  /**
200
297
  * Check whether an anyOf/oneOf array contains a `{ type: "null" }` member.
201
298
  *
@@ -212,12 +309,12 @@ type HasNullMember<A> = A extends readonly unknown[] ? null extends A[number] ?
212
309
  * Dispatch on a `type` value -- handles single types, type arrays,
213
310
  * and delegates to the appropriate type-specific resolver.
214
311
  */
215
- type TypeToTs<T, S, Defs extends Record<string, unknown>> = T extends "string" ? string : T extends "number" | "integer" ? number : T extends "boolean" ? boolean : T extends "null" ? null : T extends "array" ? ArraySchemaToTs<S, Defs> : T extends "object" ? ObjectSchemaToTs<S, Defs> : T extends readonly (infer E)[] ? TypeArrayToTs<E, S, Defs> : unknown;
312
+ type TypeToTs<T, S, Defs extends Record<string, unknown>, Depth extends readonly unknown[] = []> = T extends "string" ? string : T extends "number" | "integer" ? number : T extends "boolean" ? boolean : T extends "null" ? null : T extends "array" ? ArraySchemaToTs<S, Defs, Depth> : T extends "object" ? ObjectSchemaToTs<S, Defs, Depth> : T extends readonly (infer E)[] ? TypeArrayToTs<E, S, Defs, Depth> : unknown;
216
313
  /**
217
314
  * Handle `type` as an array (Draft 04-07): `["string", "null"]`.
218
315
  * Filters out "null" and makes the result nullable.
219
316
  */
220
- type TypeArrayToTs<E, S, Defs extends Record<string, unknown>> = E extends "null" ? null : E extends "string" ? NullableResult<string, S> : E extends "number" | "integer" ? NullableResult<number, S> : E extends "boolean" ? NullableResult<boolean, S> : E extends "array" ? NullableResult<ArraySchemaToTs<OmitArrayHelpers<S>, Defs>, S> : E extends "object" ? NullableResult<ObjectSchemaToTs<OmitArrayHelpers<S>, Defs>, S> : unknown;
317
+ type TypeArrayToTs<E, S, Defs extends Record<string, unknown>, Depth extends readonly unknown[] = []> = E extends "null" ? null : E extends "string" ? NullableResult<string, S> : E extends "number" | "integer" ? NullableResult<number, S> : E extends "boolean" ? NullableResult<boolean, S> : E extends "array" ? NullableResult<ArraySchemaToTs<OmitArrayHelpers<S>, Defs, Depth>, S> : E extends "object" ? NullableResult<ObjectSchemaToTs<OmitArrayHelpers<S>, Defs, Depth>, S> : unknown;
221
318
  /**
222
319
  * Make a type nullable if the original schema `type` array includes "null".
223
320
  * Detects nullable from the type array directly.
@@ -243,17 +340,17 @@ type OmitArrayHelpers<S> = Omit<S, "prefixItems" | "items" | "additionalProperti
243
340
  * `contains` / `minContains` / `maxContains` constrain elements at runtime
244
341
  * but don't change the compile-time array element type.
245
342
  */
246
- type ArraySchemaToTs<S, Defs extends Record<string, unknown>> = S extends {
343
+ type ArraySchemaToTs<S, Defs extends Record<string, unknown>, Depth extends readonly unknown[] = []> = S extends {
247
344
  prefixItems: infer P;
248
- } ? PrefixItemsToTuple<P, Defs> : S extends {
345
+ } ? PrefixItemsToTuple<P, Defs, Depth> : S extends {
249
346
  items: infer I extends readonly unknown[];
250
- } ? PrefixItemsToTuple<I, Defs> : S extends {
347
+ } ? PrefixItemsToTuple<I, Defs, Depth> : S extends {
251
348
  items: infer I;
252
- } ? FromJSONSchema<I, Defs>[] : unknown[];
349
+ } ? FromJSONSchema<I, Defs, Depth>[] : unknown[];
253
350
  /**
254
351
  * Convert a prefixItems array to a TypeScript tuple type.
255
352
  */
256
- type PrefixItemsToTuple<P, Defs extends Record<string, unknown>> = P extends readonly [infer First, ...infer Rest] ? [FromJSONSchema<First, Defs>, ...PrefixItemsToTuple<Rest, Defs>] : [];
353
+ type PrefixItemsToTuple<P, Defs extends Record<string, unknown>, Depth extends readonly unknown[] = []> = P extends readonly [infer First, ...infer Rest] ? [FromJSONSchema<First, Defs, Depth>, ...PrefixItemsToTuple<Rest, Defs, Depth>] : [];
257
354
  /**
258
355
  * Parse an object schema: properties + required -> specific object,
259
356
  * additionalProperties -> Record, or empty object.
@@ -267,24 +364,24 @@ type PrefixItemsToTuple<P, Defs extends Record<string, unknown>> = P extends rea
267
364
  * - `dependentSchemas` / `dependentRequired` -> ignored (runtime-only conditionals)
268
365
  * - `unevaluatedProperties` -> ignored (runtime-only)
269
366
  */
270
- type ObjectSchemaToTs<S, Defs extends Record<string, unknown>> = S extends {
367
+ type ObjectSchemaToTs<S, Defs extends Record<string, unknown>, Depth extends readonly unknown[] = []> = S extends {
271
368
  type: "object";
272
369
  properties: infer P;
273
- } ? ExtractDefs<S, Defs> extends infer D extends Record<string, unknown> ? MergePatternProps<{ [K in keyof P as K extends RequiredKeysOf<S> ? K : never]: FromJSONSchema<P[K], D> } & { [K in keyof P as K extends RequiredKeysOf<S> ? never : K]?: FromJSONSchema<P[K], D> }, S, D> : never : S extends {
370
+ } ? ExtractDefs<S, Defs> extends infer D extends Record<string, unknown> ? MergePatternProps<{ [K in keyof P as K extends RequiredKeysOf<S> ? K : never]: FromJSONSchema<P[K], D, Depth> } & { [K in keyof P as K extends RequiredKeysOf<S> ? never : K]?: FromJSONSchema<P[K], D, Depth> }, S, D, Depth> : never : S extends {
274
371
  additionalProperties: infer V;
275
- } ? Record<string, FromJSONSchema<V, Defs>> : Record<string, unknown>;
372
+ } ? Record<string, FromJSONSchema<V, Defs, Depth>> : Record<string, unknown>;
276
373
  /**
277
374
  * If the schema has `patternProperties`, intersect the base object type
278
375
  * with a `Record<string, T>` index signature covering all pattern values.
279
376
  * If no `patternProperties`, return the base type unchanged.
280
377
  */
281
- type MergePatternProps<Base, S, Defs extends Record<string, unknown>> = S extends {
378
+ type MergePatternProps<Base, S, Defs extends Record<string, unknown>, Depth extends readonly unknown[] = []> = S extends {
282
379
  patternProperties: infer PP;
283
- } ? PP extends Record<string, unknown> ? Base & Record<string, UnionOfPatternValues<PP, Defs>> : Base : Base;
380
+ } ? PP extends Record<string, unknown> ? Base & Record<string, UnionOfPatternValues<PP, Defs, Depth>> : Base : Base;
284
381
  /**
285
382
  * Extract the union of all pattern property value types.
286
383
  */
287
- type UnionOfPatternValues<PP extends Record<string, unknown>, Defs extends Record<string, unknown>> = { [K in keyof PP]: FromJSONSchema<PP[K], Defs> }[keyof PP];
384
+ type UnionOfPatternValues<PP extends Record<string, unknown>, Defs extends Record<string, unknown>, Depth extends readonly unknown[] = []> = { [K in keyof PP]: FromJSONSchema<PP[K], Defs, Depth> }[keyof PP];
288
385
  /**
289
386
  * Extract the `required` array from a schema as a union of string literals.
290
387
  * Handles both readonly `as const` arrays and mutable arrays.
@@ -294,11 +391,16 @@ type RequiredKeysOf<S> = S extends {
294
391
  } ? R extends readonly string[] ? R[number] : never : never;
295
392
  /**
296
393
  * Extract $defs / definitions from a schema for $ref resolution context.
297
- * Also indexes schemas with `$anchor` or `$dynamicAnchor` by their anchor name,
298
- * enabling `#SomeName` ref resolution.
299
- * Merges with the existing Defs context from parent schemas.
394
+ * Also indexes schemas with `$anchor` or `$dynamicAnchor` by their anchor
395
+ * name, enabling `#SomeName` ref resolution.
396
+ *
397
+ * Shares merge semantics with {@link MergeRootDefs}: caller-supplied
398
+ * (`ParentDefs`) entries win on key collision, the empty-default
399
+ * sentinel is detected so it does not poison the parent context, and the
400
+ * `HasLocalDefs` guard short-circuits when the current node declares no
401
+ * defs of its own.
300
402
  */
301
- type ExtractDefs<S, ParentDefs extends Record<string, unknown>> = ExtractRawDefs<S> extends infer RawDefs extends Record<string, unknown> ? RawDefs & ParentDefs & ExtractAnchors<RawDefs> : ParentDefs;
403
+ type ExtractDefs<S, ParentDefs extends Record<string, unknown>> = HasLocalDefs<S> extends true ? ExtractRawDefs<S> extends infer RawDefs extends Record<string, unknown> ? CollisionSafeMerge<CollisionSafeMerge<RawDefs, ExtractAnchors<RawDefs>>, ParentDefs> : ParentDefs : ParentDefs;
302
404
  /** Extract raw $defs / definitions maps. */
303
405
  type ExtractRawDefs<S> = S extends {
304
406
  $defs: infer D;
@@ -441,9 +543,11 @@ type ParameterNamesOf<Doc, Path extends string, Method extends string> = Operati
441
543
  * Detect whether a document is Swagger 2.0 (OpenAPI 2.0).
442
544
  *
443
545
  * 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.
546
+ * `packages/core/src/core/version.ts` (line 305), which parses the
547
+ * `swagger` field via `detectOpenApiVersion` (line 264) and returns true
548
+ * for any document whose major version is `2`. Runtime therefore accepts
549
+ * `"2.0"`, `"2.0.0"`, `"2.1"`, and any other `2.x` form — so the
550
+ * type-level detector must too.
447
551
  *
448
552
  * Type-level Swagger 2.0 documents cannot be fully normalised at compile
449
553
  * time — the rewrite reorders the document tree (definitions →
@@ -451,9 +555,19 @@ type ParameterNamesOf<Doc, Path extends string, Method extends string> = Operati
451
555
  * TypeScript's mapped-type machinery cannot express. Detecting the
452
556
  * version is tractable, so we surface `__SchemaInferenceFellBack`
453
557
  * deliberately rather than silently producing `unknown`.
558
+ *
559
+ * Two shapes are accepted:
560
+ * - `{ swagger: "2.<anything>" }` — the on-the-wire string form
561
+ * - `{ swagger: { major: 2, ... } }` — the parsed `OpenApiVersionInfo`
562
+ * object form, mirroring the runtime's tolerance for pre-parsed
563
+ * version metadata
454
564
  */
455
565
  type IsSwagger2Doc<Doc> = Doc extends {
456
- swagger: "2.0";
566
+ swagger: `2.${string}`;
567
+ } ? true : Doc extends {
568
+ swagger: {
569
+ major: 2;
570
+ };
457
571
  } ? true : false;
458
572
  /**
459
573
  * Infer the TypeScript type of an OpenAPI operation's request body.
@@ -84,8 +84,6 @@ interface ArrayConstraints {
84
84
  minItems?: number;
85
85
  maxItems?: number;
86
86
  uniqueItems?: boolean;
87
- /** Schema that at least one array item must match. */
88
- contains?: Record<string, unknown>;
89
87
  minContains?: number;
90
88
  maxContains?: number;
91
89
  /** Constraint schema for unevaluated items. */
@@ -176,6 +174,12 @@ interface ArrayField extends FieldBase {
176
174
  constraints: ArrayConstraints;
177
175
  /** The element sub-schema. */
178
176
  element?: WalkedField;
177
+ /**
178
+ * Walked schema that at least one array item must match
179
+ * (`contains` keyword). Constrains element membership at runtime;
180
+ * paired with `minContains`/`maxContains` for cardinality.
181
+ */
182
+ contains?: WalkedField;
179
183
  /** Walked schema for unevaluated items. */
180
184
  unevaluatedItems?: WalkedField;
181
185
  }
@@ -190,6 +194,12 @@ interface TupleField extends FieldBase {
190
194
  * absent, additional items are permitted but unconstrained.
191
195
  */
192
196
  restItems?: WalkedField;
197
+ /**
198
+ * Walked schema that at least one array item must match
199
+ * (`contains` keyword). Tuples may declare it alongside positional
200
+ * element schemas to require the presence of a specific element.
201
+ */
202
+ contains?: WalkedField;
193
203
  }
194
204
  interface RecordField extends FieldBase {
195
205
  type: "record";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schema-components",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "description": "React components that render UI from Zod schemas, JSON Schema, and OpenAPI documents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",