@kubb/adapter-oas 5.0.0-alpha.16 → 5.0.0-alpha.18

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.
package/src/parser.ts CHANGED
@@ -1,23 +1,26 @@
1
1
  import { URLPath } from '@internals/utils'
2
2
  import {
3
- applyDiscriminatorEnum,
3
+ childName,
4
+ createDiscriminantNode,
4
5
  createOperation,
5
6
  createParameter,
6
7
  createProperty,
7
8
  createResponse,
8
9
  createRoot,
9
10
  createSchema,
11
+ enumPropName,
10
12
  extractRefName,
11
- mediaTypes,
12
- mergeAdjacentAnonymousObjects,
13
+ findDiscriminator,
14
+ mergeAdjacentObjects,
13
15
  narrowSchema,
14
- schemaTypes,
15
- simplifyUnionMembers,
16
+ type ParserOptions,
17
+ setDiscriminatorEnum,
18
+ setEnumName,
19
+ simplifyUnion,
16
20
  } from '@kubb/ast'
17
21
  import type {
18
22
  DistributiveOmit,
19
23
  HttpMethod,
20
- MediaType,
21
24
  OperationNode,
22
25
  ParameterLocation,
23
26
  ParameterNode,
@@ -27,61 +30,44 @@ import type {
27
30
  RootNode,
28
31
  ScalarSchemaType,
29
32
  SchemaNode,
30
- SchemaType,
31
33
  StatusCode,
32
34
  } from '@kubb/ast/types'
33
- import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, formatMap } from './constants.ts'
34
- import { createDiscriminantNode, resolveDiscriminatorValue } from './discriminator.ts'
35
- import type { InferSchemaNode } from './infer.ts'
36
- import { applyEnumName, resolveChildName, resolveEnumPropName } from './naming.ts'
37
- import type { Oas } from './oas/Oas.ts'
38
- import type { contentType, Operation, ReferenceObject, SchemaObject } from './oas/types.ts'
39
- import { flattenSchema, isDiscriminator, isNullable, isReference } from './oas/utils.ts'
40
- import { resolveRefs as resolveRefsNode } from './refResolver.ts'
41
- import type { ParserOptions } from './types.ts'
35
+ import BaseOas from 'oas'
36
+ import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, typeOptionMap } from './constants.ts'
37
+ import { isDiscriminator, isNullable, isReference } from './guards.ts'
38
+ import { resolveRef } from './refs.ts'
39
+ import {
40
+ buildSchemaNode,
41
+ flattenSchema,
42
+ getDateType,
43
+ getMediaType,
44
+ getParameters,
45
+ getPrimitiveType,
46
+ getRequestSchema,
47
+ getResponseSchema,
48
+ getSchemas,
49
+ getSchemaType,
50
+ } from './resolvers.ts'
51
+ import type { contentType, Document, Operation, ReferenceObject, SchemaObject } from './types.ts'
42
52
 
43
53
  /**
44
- * Construction-time options for `createOasParser`.
54
+ * Construction-time context for the OAS parser.
55
+ *
56
+ * Holds the raw OpenAPI document and optional content-type override used when extracting
57
+ * request/response schemas.
45
58
  */
46
- export type OasParserOptions = {
59
+ export type OasParserContext = {
60
+ document: Document
47
61
  contentType?: contentType
48
62
  }
49
63
 
50
- /**
51
- * Looks up the Kubb `SchemaType` for a given OAS `format` string.
52
- * Returns `undefined` for formats not in `formatMap` (e.g. `int64`, `date-time`),
53
- * which are handled separately because their output depends on parser options.
54
- */
55
- function formatToSchemaType(format: string): SchemaType | undefined {
56
- return formatMap[format as keyof typeof formatMap]
57
- }
58
-
59
- /**
60
- * Maps an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
61
- * Numeric types (`number`, `integer`, `bigint`) are returned unchanged;
62
- * `boolean` maps to `'boolean'`; everything else defaults to `'string'`.
63
- */
64
- function getPrimitiveType(type: string | undefined): PrimitiveSchemaType {
65
- if (type === 'number' || type === 'integer' || type === 'bigint') return type
66
- if (type === 'boolean') return 'boolean'
67
- return 'string'
68
- }
69
-
70
- /**
71
- * Narrows a raw content-type string to the `MediaType` union recognized by Kubb.
72
- * Returns `undefined` for content types not present in `KNOWN_MEDIA_TYPES`.
73
- */
74
- function toMediaType(contentType: string): MediaType | undefined {
75
- return Object.values(mediaTypes).includes(contentType as MediaType) ? (contentType as MediaType) : undefined
76
- }
77
-
78
64
  /**
79
65
  * Pre-computed per-schema context passed to every `convert*` branch handler.
80
66
  * Grouping these values avoids repeating the same derivations across all branches.
81
67
  */
82
68
  type SchemaContext = {
83
69
  schema: SchemaObject
84
- name: string | undefined
70
+ name: string | null | undefined
85
71
  nullable: true | undefined
86
72
  defaultValue: unknown
87
73
  /**
@@ -93,144 +79,31 @@ type SchemaContext = {
93
79
  }
94
80
 
95
81
  /**
96
- * The public interface returned by `createOasParser`.
82
+ * Normalize a malformed `{ type: 'array', enum: [...] }` schema by moving the
83
+ * enum values into the items sub-schema. This pattern is technically invalid OAS
84
+ * but appears in the wild and must be handled gracefully.
97
85
  */
98
- export type OasParser = {
99
- /**
100
- * Converts an OpenAPI/Swagger spec (wrapped in a Kubb `Oas` instance) into
101
- * a `RootNode` the top-level node of the `@kubb/ast` tree.
102
- */
103
- parse: <TOptions extends Partial<ParserOptions> = object>(options?: TOptions) => RootNode
104
- convertSchema: <TFormat extends string, TSchema extends SchemaObject & { format?: TFormat }, TOptions extends Partial<ParserOptions> = object>(
105
- params: { schema: TSchema; name?: string },
106
- options?: TOptions,
107
- ) => InferSchemaNode<TSchema, TOptions extends { dateType: ParserOptions['dateType'] } ? TOptions['dateType'] : 'string'>
108
- /**
109
- * Walks `node` and replaces each `ref` value with the name returned by
110
- * `resolveName`. The callback receives the full `$ref` path (e.g. `#/components/schemas/Order`)
111
- * when available, falling back to the short name. Pass a no-op (`(n) => n`) to skip resolution.
112
- *
113
- * The optional `resolveEnumName` callback is called for inline `enum` nodes and should return
114
- * the transformed name to use (e.g. with a plugin `transformers.name` applied).
115
- */
116
- resolveRefs: (node: SchemaNode, resolveName: (ref: string) => string | undefined, resolveEnumName?: (name: string) => string | undefined) => SchemaNode
86
+ function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
87
+ const isItemsObject = typeof schema.items === 'object' && !Array.isArray(schema.items)
88
+ const normalizedItems: SchemaObject = { ...(isItemsObject ? (schema.items as SchemaObject) : {}), enum: schema.enum }
89
+ const { enum: _enum, ...schemaWithoutEnum } = schema
117
90
 
118
- /**
119
- * Map from original `$ref` paths to their collision-resolved schema names.
120
- * e.g. `'#/components/schemas/Order'` → `'OrderSchema'`
121
- *
122
- * Pass this to the standalone `getImports()` to resolve imports without holding
123
- * a reference to the full parser or OAS instance.
124
- */
125
- nameMapping: Map<string, string>
91
+ return { ...schemaWithoutEnum, items: normalizedItems } as SchemaObject
126
92
  }
127
93
 
128
94
  /**
129
- * Creates an OAS parser that converts an OpenAPI/Swagger spec into
130
- * the `@kubb/ast` tree.
131
- *
132
- * Options are passed per-call to `parse` or `convertSchema` rather than
133
- * at construction time, keeping the factory lightweight.
95
+ * Builds the internal converter functions for a given `OasParserContext`.
134
96
  *
135
- * This is the **kubb-parser** stage of the compilation lifecycle:
136
- * OpenAPI / Swagger → Kubb AST
137
- *
138
- * No code is generated here; the resulting tree is spec-agnostic and can
139
- * be consumed by any downstream plugin (plugin-ts, plugin-zod, …).
140
- *
141
- * @example
142
- * ```ts
143
- * const parser = createOasParser(oas)
144
- * const root = parser.parse({ emptySchemaType: 'unknown' })
145
- * ```
97
+ * All `convert*` functions are defined as function declarations so they can freely
98
+ * reference each other and `parseSchema` via JS hoisting (mutual recursion).
146
99
  */
147
- export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}): OasParser {
148
- // Map from original component paths to resolved schema names (after collision resolution)
149
- // e.g., { '#/components/schemas/Order': 'OrderSchema', '#/components/responses/Product': 'ProductResponse' }
150
- const { schemas: schemaObjects, nameMapping } = oas.getSchemas({ contentType })
151
-
152
- const TYPE_OPTION_MAP: Record<'any' | 'unknown' | 'void', ScalarSchemaType> = {
153
- any: schemaTypes.any,
154
- unknown: schemaTypes.unknown,
155
- void: schemaTypes.void,
156
- }
157
-
158
- /**
159
- * Maps an `'any' | 'unknown' | 'void'` option string to the corresponding `SchemaType` constant.
160
- * Used for both `unknownType` (unannotated schemas) and `emptySchemaType` (empty `{}` schemas).
161
- */
162
- function resolveTypeOption(value: 'any' | 'unknown' | 'void'): ScalarSchemaType {
163
- return TYPE_OPTION_MAP[value]
164
- }
165
-
166
- function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
167
- const isItemsObject = typeof schema.items === 'object' && !Array.isArray(schema.items)
168
- const normalizedItems: SchemaObject = { ...(isItemsObject ? (schema.items as SchemaObject) : {}), enum: schema.enum }
169
- const { enum: _enum, ...schemaWithoutEnum } = schema
170
- return { ...schemaWithoutEnum, items: normalizedItems } as SchemaObject
171
- }
172
-
173
- /**
174
- * Resolves the AST type and datetime modifiers for a date/time format, honoring the `dateType` option.
175
- * Returns `undefined` when `dateType` is `false`, meaning the format should fall through to `string`.
176
- */
177
- function getDateType(
178
- options: ParserOptions,
179
- format: 'date-time' | 'date' | 'time',
180
- ): { type: 'datetime'; offset?: boolean; local?: boolean } | { type: 'date' | 'time'; representation: 'date' | 'string' } | undefined {
181
- if (!options.dateType) {
182
- return undefined
183
- }
184
-
185
- if (format === 'date-time') {
186
- if (options.dateType === 'date') {
187
- return { type: 'date', representation: 'date' }
188
- }
189
- if (options.dateType === 'stringOffset') {
190
- return { type: 'datetime', offset: true }
191
- }
192
- if (options.dateType === 'stringLocal') {
193
- return { type: 'datetime', local: true }
194
- }
195
- return { type: 'datetime', offset: false }
196
- }
197
-
198
- if (format === 'date') {
199
- return { type: 'date', representation: options.dateType === 'date' ? 'date' : 'string' }
200
- }
201
-
202
- // time
203
- return { type: 'time', representation: options.dateType === 'date' ? 'date' : 'string' }
204
- }
205
-
206
- /**
207
- * Shared metadata fields included in every `createSchema` call.
208
- * Centralizes the common properties so sub-handlers don't repeat them.
209
- */
210
- function renderSchemaBase(schema: SchemaObject, name: string | undefined, nullable: true | undefined, defaultValue: unknown) {
211
- return {
212
- name,
213
- nullable,
214
- title: schema.title,
215
- description: schema.description,
216
- deprecated: schema.deprecated,
217
- readOnly: schema.readOnly,
218
- writeOnly: schema.writeOnly,
219
- default: defaultValue,
220
- example: schema.example,
221
- } as const
222
- }
100
+ function createSchemaParser(ctx: OasParserContext) {
101
+ const document = ctx.document
223
102
 
224
103
  // Branch handlers — each converts one OAS schema pattern to a SchemaNode.
225
- // They are defined as function declarations so they can reference each other
226
- // and `convertSchema` freely (JS hoisting).
227
104
 
228
105
  /**
229
- * Converts a `$ref` schema pointer into a `RefSchemaNode`.
230
- *
231
- * In OAS 3.0 siblings of `$ref` are technically ignored by the spec, but Kubb intentionally
232
- * preserves them so that annotations like `pattern`, `description`, and `nullable` are
233
- * reflected in generated JSDoc and type modifiers.
106
+ * Converts a `$ref` schema into a `RefSchemaNode`.
234
107
  */
235
108
  function convertRef({ schema, nullable, defaultValue }: SchemaContext): SchemaNode {
236
109
  return createSchema({
@@ -249,19 +122,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
249
122
  }
250
123
 
251
124
  /**
252
- * Converts a `allOf` schema into either a flattened member node (single-member `allOf`)
253
- * or an `IntersectionSchemaNode` (multi-member `allOf`).
254
- *
255
- * Single-member `allOf` without sibling structural keys is the common OAS 3.0 pattern for
256
- * annotating a `$ref` or primitive with extra constraints; it is flattened to avoid
257
- * producing needless intersection wrappers.
258
- *
259
- * The flatten path is skipped when the outer schema carries structural keys that cannot be
260
- * merged into annotation fields: `properties`, `required`, or `additionalProperties`.
261
- * Those cases must become an intersection so the constraints are preserved.
262
- *
263
- * Circular references through discriminator parents are detected and skipped to prevent
264
- * infinite recursion during code generation.
125
+ * Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
265
126
  */
266
127
  function convertAllOf({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
267
128
  if (
@@ -271,7 +132,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
271
132
  schema.additionalProperties === undefined
272
133
  ) {
273
134
  const [memberSchema] = schema.allOf as Array<SchemaObject | ReferenceObject>
274
- const memberNode = convertSchema({ schema: memberSchema! as SchemaObject }, rawOptions)
135
+ const memberNode = parseSchema({ schema: memberSchema! as SchemaObject, name: null }, rawOptions)
275
136
  const { kind: _kind, ...memberNodeProps } = memberNode
276
137
  const mergedNullable = nullable || memberNode.nullable || undefined
277
138
  const mergedDefault = schema.default === null && mergedNullable ? undefined : (schema.default ?? memberNode.default)
@@ -291,14 +152,11 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
291
152
  } as DistributiveOmit<SchemaNode, 'kind'>)
292
153
  }
293
154
 
294
- // When a child schema extends a discriminator parent via allOf and the parent's oneOf/anyOf
295
- // references that child back, skip that allOf item to prevent a circular type reference.
296
- // When an item is skipped, collect its discriminant value so it can be injected below.
297
155
  const filteredDiscriminantValues: Array<{ propertyName: string; value: string }> = []
298
156
  const allOfMembers: Array<SchemaNode> = (schema.allOf as Array<SchemaObject | ReferenceObject>)
299
157
  .filter((item) => {
300
158
  if (!isReference(item) || !name) return true
301
- const deref = oas.get<SchemaObject>(item.$ref)
159
+ const deref = resolveRef<SchemaObject>(document, item.$ref)
302
160
  if (!deref || !isDiscriminator(deref)) return true
303
161
  const parentUnion = deref.oneOf ?? deref.anyOf
304
162
  if (!parentUnion) return true
@@ -306,7 +164,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
306
164
  const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef)
307
165
  const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef)
308
166
  if (inOneOf || inMapping) {
309
- const discriminatorValue = resolveDiscriminatorValue(deref.discriminator.mapping, childRef)
167
+ const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef)
310
168
  if (discriminatorValue) {
311
169
  filteredDiscriminantValues.push({ propertyName: deref.discriminator.propertyName, value: discriminatorValue })
312
170
  }
@@ -314,13 +172,10 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
314
172
  }
315
173
  return true
316
174
  })
317
- .map((s) => convertSchema({ schema: s as SchemaObject }, rawOptions))
175
+ .map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))
318
176
 
319
- // Track where allOf-derived members end so each portion can be merged independently.
320
177
  const syntheticStart = allOfMembers.length
321
178
 
322
- // When `required` lists keys not present in the outer `properties`, resolve them from
323
- // the allOf member schemas and inject them as extra intersection members.
324
179
  if (Array.isArray(schema.required) && schema.required.length) {
325
180
  const outerKeys = schema.properties ? new Set(Object.keys(schema.properties)) : new Set<string>()
326
181
  const missingRequired = schema.required.filter((key) => !outerKeys.has(key))
@@ -328,14 +183,14 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
328
183
  if (missingRequired.length) {
329
184
  const resolvedMembers = (schema.allOf as Array<SchemaObject | ReferenceObject>).flatMap((item) => {
330
185
  if (!isReference(item)) return [item as SchemaObject]
331
- const deref = oas.get<SchemaObject>(item.$ref)
186
+ const deref = resolveRef<SchemaObject>(document, item.$ref)
332
187
  return deref && !isReference(deref) ? [deref] : []
333
188
  })
334
189
 
335
190
  for (const key of missingRequired) {
336
191
  for (const resolved of resolvedMembers) {
337
192
  if (resolved.properties?.[key]) {
338
- allOfMembers.push(convertSchema({ schema: { properties: { [key]: resolved.properties[key] }, required: [key] } as SchemaObject }, rawOptions))
193
+ allOfMembers.push(parseSchema({ schema: { properties: { [key]: resolved.properties[key] }, required: [key] } as SchemaObject }, rawOptions))
339
194
  break
340
195
  }
341
196
  }
@@ -345,104 +200,113 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
345
200
 
346
201
  if (schema.properties) {
347
202
  const { allOf: _allOf, ...schemaWithoutAllOf } = schema
348
- allOfMembers.push(convertSchema({ schema: schemaWithoutAllOf }, rawOptions))
203
+ allOfMembers.push(parseSchema({ schema: schemaWithoutAllOf }, rawOptions))
349
204
  }
350
205
 
351
- // Inject a synthetic single-property object for each discriminant value collected from
352
- // filtered discriminator parents so that child schemas carry the narrowed literal type.
353
206
  for (const { propertyName, value } of filteredDiscriminantValues) {
354
- allOfMembers.push(createDiscriminantNode(propertyName, value))
207
+ allOfMembers.push(createDiscriminantNode({ propertyName, value }))
355
208
  }
356
209
 
357
- // Merge consecutive anonymous object members within the synthetic portion — see `mergeAdjacentAnonymousObjects`.
358
210
  return createSchema({
359
211
  type: 'intersection',
360
- members: [...mergeAdjacentAnonymousObjects(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentAnonymousObjects(allOfMembers.slice(syntheticStart))],
361
- ...renderSchemaBase(schema, name, nullable, defaultValue),
212
+ members: [...mergeAdjacentObjects(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentObjects(allOfMembers.slice(syntheticStart))],
213
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
362
214
  })
363
215
  }
364
216
 
365
217
  /**
366
218
  * Converts a `oneOf` / `anyOf` schema into a `UnionSchemaNode`.
367
- *
368
- * Both keywords are treated identically — their members are concatenated into a single union.
369
- * When sibling `properties` are present alongside `oneOf`/`anyOf`, each union member is
370
- * individually intersected with the shared properties node to match the OAS pattern of
371
- * adding common fields next to a discriminated union.
372
219
  */
373
220
  function convertUnion({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
221
+ function pickDiscriminatorPropertyNode(node: SchemaNode, propertyName: string): SchemaNode | null {
222
+ const objectNode = narrowSchema(node, 'object')
223
+ const discriminatorProperty = objectNode?.properties?.find((property) => property.name === propertyName)
224
+
225
+ if (!discriminatorProperty) {
226
+ return null
227
+ }
228
+
229
+ return createSchema({
230
+ type: 'object',
231
+ primitive: 'object',
232
+ properties: [discriminatorProperty],
233
+ })
234
+ }
235
+
374
236
  const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
375
237
  const unionBase = {
376
- ...renderSchemaBase(schema, name, nullable, defaultValue),
238
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
377
239
  discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
378
240
  }
379
241
  const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
380
242
  const sharedPropertiesNode = schema.properties
381
243
  ? (() => {
382
244
  const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
383
- // Strip discriminator so convertObject won't re-apply the full mapping enum.
384
245
  const memberBaseSchema: SchemaObject = discriminator
385
246
  ? (Object.fromEntries(Object.entries(schemaWithoutUnion).filter(([key]) => key !== 'discriminator')) as SchemaObject)
386
247
  : schemaWithoutUnion
387
- // Convert shared properties once to avoid duplicate enum naming
388
- // (e.g. StatusEnum appearing twice and getting a numeric suffix).
389
- return convertSchema({ schema: memberBaseSchema, name }, rawOptions)
248
+ return parseSchema({ schema: memberBaseSchema, name }, rawOptions)
390
249
  })()
391
250
  : undefined
392
251
 
393
252
  if (sharedPropertiesNode || discriminator?.mapping) {
394
- return createSchema({
253
+ const members = unionMembers.map((s) => {
254
+ const ref = isReference(s) ? s.$ref : undefined
255
+ const discriminatorValue = findDiscriminator(discriminator?.mapping, ref)
256
+ const memberNode = parseSchema({ schema: s as SchemaObject }, rawOptions)
257
+
258
+ if (!discriminatorValue || !discriminator) {
259
+ return memberNode
260
+ }
261
+
262
+ const narrowedDiscriminatorNode = sharedPropertiesNode
263
+ ? pickDiscriminatorPropertyNode(
264
+ setDiscriminatorEnum({
265
+ node: sharedPropertiesNode,
266
+ propertyName: discriminator.propertyName,
267
+ values: [discriminatorValue],
268
+ }),
269
+ discriminator.propertyName,
270
+ )
271
+ : undefined
272
+
273
+ return createSchema({
274
+ type: 'intersection',
275
+ members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({ propertyName: discriminator.propertyName, value: discriminatorValue })],
276
+ })
277
+ })
278
+
279
+ const unionNode = createSchema({
395
280
  type: 'union',
396
281
  ...unionBase,
397
- members: unionMembers.map((s) => {
398
- const ref = isReference(s) ? s.$ref : undefined
399
- const discriminatorValue = resolveDiscriminatorValue(discriminator?.mapping, ref)
400
- const memberNode = convertSchema({ schema: s as SchemaObject }, rawOptions)
401
-
402
- if (sharedPropertiesNode) {
403
- const narrowedProperties =
404
- discriminatorValue && discriminator
405
- ? applyDiscriminatorEnum({
406
- node: sharedPropertiesNode,
407
- propertyName: discriminator.propertyName,
408
- values: [discriminatorValue],
409
- })
410
- : sharedPropertiesNode
411
-
412
- return createSchema({
413
- type: 'intersection',
414
- members: [memberNode, narrowedProperties],
415
- })
416
- }
282
+ members,
283
+ })
417
284
 
418
- if (!discriminatorValue || !discriminator) return memberNode
285
+ if (!sharedPropertiesNode) {
286
+ return unionNode
287
+ }
419
288
 
420
- return createSchema({
421
- type: 'intersection',
422
- members: [memberNode, createDiscriminantNode(discriminator.propertyName, discriminatorValue)],
423
- })
424
- }),
289
+ return createSchema({
290
+ type: 'intersection',
291
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
292
+ members: [unionNode, sharedPropertiesNode],
425
293
  })
426
294
  }
427
295
 
428
296
  return createSchema({
429
297
  type: 'union',
430
298
  ...unionBase,
431
- members: simplifyUnionMembers(unionMembers.map((s) => convertSchema({ schema: s as SchemaObject }, rawOptions))),
299
+ members: simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))),
432
300
  })
433
301
  }
434
302
 
435
303
  /**
436
- * Converts an OAS 3.1 `const` schema into either a null scalar or a single-value `EnumSchemaNode`.
437
- * `const: null` maps to a null scalar; any other value becomes a one-item enum so that generators
438
- * can produce a precise literal type.
304
+ * Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
439
305
  */
440
306
  function convertConst({ schema, name, nullable, defaultValue }: SchemaContext): SchemaNode {
441
307
  const constValue = schema.const
442
308
 
443
309
  if (constValue === null) {
444
- // Do not propagate `nullable` here: the type is already `null`, so marking it
445
- // nullable too would cause the printer to emit `null | null`.
446
310
  return createSchema({
447
311
  type: 'null',
448
312
  primitive: 'null',
@@ -458,19 +322,17 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
458
322
  type: 'enum',
459
323
  primitive: constPrimitive,
460
324
  enumValues: [constValue as string | number | boolean],
461
- ...renderSchemaBase(schema, name, nullable, defaultValue),
325
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
462
326
  })
463
327
  }
464
328
 
465
329
  /**
466
- * Handles `format`-based special types (date/time, uuid, email, blob, etc.).
467
- * Returns `undefined` when the format should fall through to string handling
468
- * (i.e. `format: 'date-time'` with `dateType: false`).
330
+ * Converts a format-annotated schema into a special-type `SchemaNode`.
331
+ * Returns `null` when the format should fall through to string handling (`dateType: false`).
469
332
  */
470
- function convertFormat({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode | undefined {
471
- const base = renderSchemaBase(schema, name, nullable, defaultValue)
333
+ function convertFormat({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode | null {
334
+ const base = buildSchemaNode(schema, name, nullable, defaultValue)
472
335
 
473
- // int64 is option-dependent so it can't live in the static formatMap.
474
336
  if (schema.format === 'int64') {
475
337
  return createSchema({
476
338
  type: options.integerType === 'bigint' ? 'bigint' : 'integer',
@@ -483,10 +345,9 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
483
345
  })
484
346
  }
485
347
 
486
- // date-time / date / time are option-dependent and can't live in the static formatMap.
487
348
  if (schema.format === 'date-time' || schema.format === 'date' || schema.format === 'time') {
488
349
  const dateType = getDateType(options, schema.format)
489
- if (!dateType) return undefined // dateType: false → fall through to string
350
+ if (!dateType) return null
490
351
 
491
352
  if (dateType.type === 'datetime') {
492
353
  return createSchema({ ...base, primitive: 'string' as const, type: 'datetime', offset: dateType.offset, local: dateType.local })
@@ -494,8 +355,8 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
494
355
  return createSchema({ ...base, primitive: 'string' as const, type: dateType.type, representation: dateType.representation })
495
356
  }
496
357
 
497
- const specialType = formatToSchemaType(schema.format!)
498
- if (!specialType) return undefined
358
+ const specialType = getSchemaType(schema.format!)
359
+ if (!specialType) return null
499
360
 
500
361
  const specialPrimitive: PrimitiveSchemaType = specialType === 'number' || specialType === 'integer' || specialType === 'bigint' ? specialType : 'string'
501
362
 
@@ -511,21 +372,12 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
511
372
 
512
373
  /**
513
374
  * Converts an `enum` schema into an `EnumSchemaNode`.
514
- *
515
- * Handles several edge cases:
516
- * - `{ type: 'array', enum }` (technically invalid OAS) — the enum is normalized into `items`.
517
- * - `null` in enum values (OAS 3.0 nullable enum convention) — stripped and reflected as `nullable`.
518
- * - `x-enumNames` / `x-enum-varnames` vendor extensions — produce named enum variants with explicit labels.
519
- * - Numeric and boolean enums require a const-map representation because most generators cannot
520
- * use string-enum syntax for non-string values.
521
375
  */
522
376
  function convertEnum({ schema, name, nullable, type, rawOptions }: SchemaContext): SchemaNode {
523
- // Malformed schema: `{ type: 'array', enum: [...] }` — normalize by moving the enum into items.
524
377
  if (type === 'array') {
525
- return convertSchema({ schema: normalizeArrayEnum(schema), name }, rawOptions)
378
+ return parseSchema({ schema: normalizeArrayEnum(schema), name }, rawOptions)
526
379
  }
527
380
 
528
- // `null` in enum values is the OAS 3.0 convention for a nullable enum.
529
381
  const nullInEnum = schema.enum!.includes(null)
530
382
  const filteredValues = (nullInEnum ? schema.enum!.filter((v) => v !== null) : schema.enum!) as Array<string | number | boolean>
531
383
  const enumNullable = nullable || nullInEnum || undefined
@@ -546,10 +398,9 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
546
398
  example: schema.example,
547
399
  }
548
400
 
549
- // x-enumNames / x-enum-varnames: named variants with explicit labels take priority.
550
401
  const extensionKey = enumExtensionKeys.find((key) => key in schema)
551
402
  if (extensionKey || enumPrimitive === 'number' || enumPrimitive === 'integer' || enumPrimitive === 'boolean') {
552
- const enumType = (enumPrimitive === 'number' || enumPrimitive === 'integer' ? 'number' : enumPrimitive === 'boolean' ? 'boolean' : 'string') as
403
+ const enumPrimitiveType = (enumPrimitive === 'number' || enumPrimitive === 'integer' ? 'number' : enumPrimitive === 'boolean' ? 'boolean' : 'string') as
553
404
  | 'number'
554
405
  | 'boolean'
555
406
  | 'string'
@@ -559,16 +410,15 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
559
410
 
560
411
  return createSchema({
561
412
  ...enumBase,
562
- enumType,
413
+ primitive: enumPrimitiveType,
563
414
  namedEnumValues: sourceValues.map((label, index) => ({
564
415
  name: String(label),
565
416
  value: extensionKey ? (filteredValues[index] ?? label) : label,
566
- format: enumType,
417
+ primitive: enumPrimitiveType,
567
418
  })),
568
419
  })
569
420
  }
570
421
 
571
- // Plain string enum (default path).
572
422
  return createSchema({
573
423
  ...enumBase,
574
424
  enumValues: [...new Set(filteredValues)],
@@ -576,18 +426,8 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
576
426
  }
577
427
 
578
428
  /**
579
- * Converts an object-like schema (`type: 'object'`, `properties`, `additionalProperties`,
580
- * or `patternProperties`) into an `ObjectSchemaNode`.
581
- *
582
- * When a `discriminator` is present, the discriminator property's schema is replaced with an
583
- * enum of the mapping keys so generators can produce a precise literal-union type for it.
584
- *
585
- * Property optionality follows OAS semantics:
586
- * - required + not nullable → `required: true`
587
- * - not required + not nullable → `optional: true`
588
- * - not required + nullable → `nullish: true`
429
+ * Converts an object-like schema into an `ObjectSchemaNode`.
589
430
  */
590
-
591
431
  function convertObject({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): SchemaNode {
592
432
  const properties: Array<PropertyNode> = schema.properties
593
433
  ? Object.entries(schema.properties).map(([propName, propSchema]) => {
@@ -595,13 +435,13 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
595
435
  const resolvedPropSchema = propSchema as SchemaObject
596
436
  const propNullable = isNullable(resolvedPropSchema)
597
437
 
598
- const childName = resolveChildName(name, propName)
599
- const propNode = convertSchema({ schema: resolvedPropSchema, name: childName }, rawOptions)
600
- let schemaNode = applyEnumName(propNode, name, propName, options.enumSuffix)
438
+ const resolvedChildName = childName(name, propName)
439
+ const propNode = parseSchema({ schema: resolvedPropSchema, name: resolvedChildName }, rawOptions)
440
+ let schemaNode = setEnumName(propNode, name, propName, options.enumSuffix)
601
441
 
602
442
  const tupleNode = narrowSchema(schemaNode, 'tuple')
603
443
  if (tupleNode?.items) {
604
- const namedItems = tupleNode.items.map((item) => applyEnumName(item, name, propName, options.enumSuffix))
444
+ const namedItems = tupleNode.items.map((item) => setEnumName(item, name, propName, options.enumSuffix))
605
445
  if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
606
446
  schemaNode = { ...tupleNode, items: namedItems }
607
447
  }
@@ -623,11 +463,11 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
623
463
  if (additionalProperties === true) {
624
464
  additionalPropertiesNode = true
625
465
  } else if (additionalProperties && Object.keys(additionalProperties).length > 0) {
626
- additionalPropertiesNode = convertSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
466
+ additionalPropertiesNode = parseSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
627
467
  } else if (additionalProperties === false) {
628
468
  additionalPropertiesNode = undefined
629
469
  } else if (additionalProperties) {
630
- additionalPropertiesNode = createSchema({ type: resolveTypeOption(options.unknownType) })
470
+ additionalPropertiesNode = createSchema({ type: typeOptionMap.get(options.unknownType)! })
631
471
  }
632
472
 
633
473
  const rawPatternProperties = 'patternProperties' in schema ? schema.patternProperties : undefined
@@ -637,8 +477,8 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
637
477
  Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [
638
478
  pattern,
639
479
  patternSchema === true || (typeof patternSchema === 'object' && Object.keys(patternSchema).length === 0)
640
- ? createSchema({ type: resolveTypeOption(options.unknownType) })
641
- : convertSchema({ schema: patternSchema as SchemaObject }, rawOptions),
480
+ ? createSchema({ type: typeOptionMap.get(options.unknownType)! })
481
+ : parseSchema({ schema: patternSchema as SchemaObject }, rawOptions),
642
482
  ]),
643
483
  )
644
484
  : undefined
@@ -649,16 +489,14 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
649
489
  properties,
650
490
  additionalProperties: additionalPropertiesNode,
651
491
  patternProperties,
652
- ...renderSchemaBase(schema, name, nullable, defaultValue),
492
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
653
493
  })
654
494
 
655
- // When a discriminator is present, replace the discriminator property's schema
656
- // with an enum of the mapping keys for a precise literal-union type.
657
495
  if (isDiscriminator(schema) && schema.discriminator.mapping) {
658
496
  const discPropName = schema.discriminator.propertyName
659
497
  const values = Object.keys(schema.discriminator.mapping)
660
- const enumName = name ? resolveEnumPropName(name, discPropName, options.enumSuffix) : undefined
661
- return applyDiscriminatorEnum({ node: objectNode, propertyName: discPropName, values, enumName })
498
+ const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : undefined
499
+ return setDiscriminatorEnum({ node: objectNode, propertyName: discPropName, values, enumName })
662
500
  }
663
501
 
664
502
  return objectNode
@@ -666,15 +504,10 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
666
504
 
667
505
  /**
668
506
  * Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
669
- *
670
- * Each `prefixItems` element maps to a positional tuple slot. When an explicit `items` schema
671
- * is present alongside `prefixItems`, it becomes the rest element. When `items` is absent,
672
- * a rest element of type `any` is emitted to match JSON Schema semantics (absent `items`
673
- * means additional items are allowed).
674
507
  */
675
508
  function convertTuple({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
676
- const tupleItems = (schema.prefixItems ?? []).map((item) => convertSchema({ schema: item as SchemaObject }, rawOptions))
677
- const rest = schema.items ? convertSchema({ schema: schema.items as SchemaObject }, rawOptions) : createSchema({ type: 'any' })
509
+ const tupleItems = (schema.prefixItems ?? []).map((item) => parseSchema({ schema: item as SchemaObject }, rawOptions))
510
+ const rest = schema.items ? parseSchema({ schema: schema.items as SchemaObject }, rawOptions) : createSchema({ type: 'any' })
678
511
 
679
512
  return createSchema({
680
513
  type: 'tuple',
@@ -683,22 +516,17 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
683
516
  rest,
684
517
  min: schema.minItems,
685
518
  max: schema.maxItems,
686
- ...renderSchemaBase(schema, name, nullable, defaultValue),
519
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
687
520
  })
688
521
  }
689
522
 
690
523
  /**
691
524
  * Converts a `type: 'array'` schema into an `ArraySchemaNode`.
692
- *
693
- * When the items schema is an inline enum, a name derived from the parent array's name and
694
- * `enumSuffix` is forwarded so generators can emit a named enum declaration.
695
525
  */
696
526
  function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): SchemaNode {
697
527
  const rawItems = schema.items as SchemaObject | undefined
698
- // When the items schema contains an inline enum, derive a named identifier
699
- // so generators can emit a standalone enum declaration.
700
- const itemName = rawItems?.enum?.length && name ? resolveEnumPropName(undefined, name, options.enumSuffix) : undefined
701
- const items = rawItems ? [convertSchema({ schema: rawItems, name: itemName }, rawOptions)] : []
528
+ const itemName = rawItems?.enum?.length && name ? enumPropName(undefined, name, options.enumSuffix) : undefined
529
+ const items = rawItems ? [parseSchema({ schema: rawItems, name: itemName }, rawOptions)] : []
702
530
 
703
531
  return createSchema({
704
532
  type: 'array',
@@ -707,12 +535,12 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
707
535
  min: schema.minItems,
708
536
  max: schema.maxItems,
709
537
  unique: schema.uniqueItems ?? undefined,
710
- ...renderSchemaBase(schema, name, nullable, defaultValue),
538
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
711
539
  })
712
540
  }
713
541
 
714
542
  /**
715
- * Converts a `type: 'string'` schema (without a special format) into a `StringSchemaNode`.
543
+ * Converts a `type: 'string'` schema into a `StringSchemaNode`.
716
544
  */
717
545
  function convertString({ schema, name, nullable, defaultValue }: SchemaContext): SchemaNode {
718
546
  return createSchema({
@@ -721,12 +549,12 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
721
549
  min: schema.minLength,
722
550
  max: schema.maxLength,
723
551
  pattern: schema.pattern,
724
- ...renderSchemaBase(schema, name, nullable, defaultValue),
552
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
725
553
  })
726
554
  }
727
555
 
728
556
  /**
729
- * Converts a `type: 'number'` or `type: 'integer'` schema into the corresponding `SchemaNode`.
557
+ * Converts a `type: 'number'` or `type: 'integer'` schema.
730
558
  */
731
559
  function convertNumeric({ schema, name, nullable, defaultValue }: SchemaContext, type: 'number' | 'integer'): SchemaNode {
732
560
  return createSchema({
@@ -736,23 +564,23 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
736
564
  max: schema.maximum,
737
565
  exclusiveMinimum: typeof schema.exclusiveMinimum === 'number' ? schema.exclusiveMinimum : undefined,
738
566
  exclusiveMaximum: typeof schema.exclusiveMaximum === 'number' ? schema.exclusiveMaximum : undefined,
739
- ...renderSchemaBase(schema, name, nullable, defaultValue),
567
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
740
568
  })
741
569
  }
742
570
 
743
571
  /**
744
- * Converts a `type: 'boolean'` schema into a `BooleanSchemaNode`.
572
+ * Converts a `type: 'boolean'` schema.
745
573
  */
746
574
  function convertBoolean({ schema, name, nullable, defaultValue }: SchemaContext): SchemaNode {
747
575
  return createSchema({
748
576
  type: 'boolean',
749
577
  primitive: 'boolean',
750
- ...renderSchemaBase(schema, name, nullable, defaultValue),
578
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
751
579
  })
752
580
  }
753
581
 
754
582
  /**
755
- * Converts an explicit `type: 'null'` or `const: null` schema into a `NullSchemaNode`.
583
+ * Converts an explicit `type: 'null'` schema.
756
584
  */
757
585
  function convertNull({ schema, name, nullable }: SchemaContext): SchemaNode {
758
586
  return createSchema({
@@ -767,67 +595,42 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
767
595
  }
768
596
 
769
597
  /**
770
- * Central dispatcher: converts an OAS `SchemaObject` into a `SchemaNode`.
598
+ * Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
771
599
  *
772
- * Dispatch order (first match wins):
773
- * 1. `$ref` pointer
774
- * 2. `allOf` composition
775
- * 3. `oneOf` / `anyOf` union
776
- * 4. `const` literal (OAS 3.1)
777
- * 5. `format`-based special type (date/time, uuid, blob, …)
778
- * 6. OAS 3.1 `contentMediaType: 'application/octet-stream'` blob
779
- * 7. OAS 3.1 multi-type array → union or fallthrough
780
- * 8. Constraint-inferred type (minLength/maxLength → string; minimum/maximum → number)
781
- * 9. `enum` values
782
- * 10. Object / array / tuple / scalar by `type`
783
- * 11. Empty schema fallback (`emptySchemaType` option)
600
+ * Dispatch order (first match wins): `$ref` → `allOf` → `oneOf`/`anyOf` → `const` → `format`
601
+ * octet-stream blob → multi-type array → constraint-inferred type → `enum` → object/array/tuple/scalar
602
+ * empty-schema fallback (`emptySchemaType` option).
784
603
  */
785
- function convertSchema({ schema, name }: { schema: SchemaObject; name?: string }, rawOptions?: Partial<ParserOptions>): SchemaNode {
604
+ function parseSchema({ schema, name }: { schema: SchemaObject; name?: string | null }, rawOptions?: Partial<ParserOptions>): SchemaNode {
786
605
  const options: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...rawOptions }
787
- // Flatten keyword-only allOf fragments (no $ref, no structural keys) into the parent
788
- // schema before parsing, so simple annotation patterns don't produce needless intersections.
789
606
  const flattenedSchema = flattenSchema(schema)
790
607
  if (flattenedSchema && flattenedSchema !== schema) {
791
- return convertSchema({ schema: flattenedSchema, name }, rawOptions)
608
+ return parseSchema({ schema: flattenedSchema, name }, rawOptions)
792
609
  }
793
610
 
794
611
  const nullable = isNullable(schema) || undefined
795
612
  const defaultValue = schema.default === null && nullable ? undefined : schema.default
796
- // Normalize OAS 3.1 multi-type array to a single type string for the dispatch below.
797
613
  const type = Array.isArray(schema.type) ? schema.type[0] : schema.type
798
614
 
799
615
  const ctx: SchemaContext = { schema, name, nullable, defaultValue, type, rawOptions, options }
800
616
 
801
- // $ref — pointer to another definition.
802
- // In OAS 3.0 siblings of $ref are technically ignored, but Kubb intentionally preserves them
803
- // so that annotations like `pattern`, `description`, and `nullable` are reflected in generated code.
804
617
  if (isReference(schema)) return convertRef(ctx)
805
618
 
806
- // Composition keywords
807
619
  if (schema.allOf?.length) return convertAllOf(ctx)
808
620
  const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
809
621
  if (unionMembers.length) return convertUnion(ctx)
810
622
 
811
- // OAS 3.1 const — a single fixed value, semantically equivalent to a one-item enum.
812
- // `const: undefined` falls through to the empty-type fallback.
813
623
  if ('const' in schema && schema.const !== undefined) return convertConst(ctx)
814
624
 
815
- // Format-based special types take precedence over `type`.
816
- // `convertFormat` returns undefined when format should fall through to string (dateType: false).
817
- // see https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7
818
625
  if (schema.format) {
819
626
  const formatResult = convertFormat(ctx)
820
627
  if (formatResult) return formatResult
821
628
  }
822
629
 
823
- // OAS 3.1: `contentMediaType: 'application/octet-stream'` on a string schema signals binary data.
824
630
  if (schema.type === 'string' && schema.contentMediaType === 'application/octet-stream') {
825
- return createSchema({ type: 'blob', primitive: 'string', ...renderSchemaBase(schema, name, nullable, defaultValue) })
631
+ return createSchema({ type: 'blob', primitive: 'string', ...buildSchemaNode(schema, name, nullable, defaultValue) })
826
632
  }
827
633
 
828
- // OAS 3.1: `type` may be an array — e.g. `["string", "integer", "null"]`.
829
- // `null` in the array is the 3.1 equivalent of `nullable: true`; strip it and set the flag.
830
- // When 2+ non-null types remain, produce a union; when exactly 1 non-null type remains, fall through.
831
634
  if (Array.isArray(schema.type) && schema.type.length > 1) {
832
635
  const nonNullTypes = schema.type.filter((t) => t !== 'null') as string[]
833
636
  const arrayNullable = schema.type.includes('null') || nullable || undefined
@@ -835,15 +638,12 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
835
638
  if (nonNullTypes.length > 1) {
836
639
  return createSchema({
837
640
  type: 'union',
838
- members: nonNullTypes.map((t) => convertSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
839
- ...renderSchemaBase(schema, name, arrayNullable, defaultValue),
641
+ members: nonNullTypes.map((t) => parseSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
642
+ ...buildSchemaNode(schema, name, arrayNullable, defaultValue),
840
643
  })
841
644
  }
842
645
  }
843
646
 
844
- // Infer type from constraints when no explicit type is provided.
845
- // minLength / maxLength / pattern → string; minimum / maximum → number.
846
- // Note: minItems/maxItems do NOT infer array — arrays require an `items` key.
847
647
  if (!type) {
848
648
  if (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined) {
849
649
  return convertString(ctx)
@@ -863,20 +663,19 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
863
663
  if (type === 'boolean') return convertBoolean(ctx)
864
664
  if (type === 'null') return convertNull(ctx)
865
665
 
866
- const emptyType = resolveTypeOption(options.emptySchemaType)
666
+ const emptyType = typeOptionMap.get(options.emptySchemaType)!
867
667
  return createSchema({ type: emptyType as ScalarSchemaType, name, title: schema.title, description: schema.description })
868
668
  }
869
669
 
870
670
  /**
871
- * Converts a single dereferenced OAS parameter object into a `ParameterNode`.
872
- * When the parameter has no `schema`, falls back to `unknownType`; `$ref` schemas are resolved through `convertSchema` to produce a proper named type reference.
671
+ * Converts a dereferenced OAS parameter object into a `ParameterNode`.
873
672
  */
874
673
  function parseParameter(options: ParserOptions, param: Record<string, unknown>): ParameterNode {
875
674
  const required = (param['required'] as boolean | undefined) ?? false
876
675
 
877
676
  const schema: SchemaNode = param['schema']
878
- ? convertSchema({ schema: param['schema'] as SchemaObject }, options)
879
- : createSchema({ type: resolveTypeOption(options.unknownType) })
677
+ ? parseSchema({ schema: param['schema'] as SchemaObject }, options)
678
+ : createSchema({ type: typeOptionMap.get(options.unknownType)! })
880
679
 
881
680
  return createParameter({
882
681
  name: param['name'] as string,
@@ -890,14 +689,15 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
890
689
  }
891
690
 
892
691
  /**
893
- * Converts an OAS `Operation` into an `OperationNode`, resolving parameters,
894
- * request body, and all response codes into their AST node equivalents.
692
+ * Converts an OAS `Operation` into an `OperationNode`.
895
693
  */
896
- function parseOperation(options: ParserOptions, oas: Oas, operation: Operation): OperationNode {
897
- const parameters: Array<ParameterNode> = oas.getParameters(operation).map((param) => parseParameter(options, param as unknown as Record<string, unknown>))
694
+ function parseOperation(options: ParserOptions, operation: Operation): OperationNode {
695
+ const parameters: Array<ParameterNode> = getParameters(document, operation).map((param) =>
696
+ parseParameter(options, param as unknown as Record<string, unknown>),
697
+ )
898
698
 
899
- const requestBodySchema = oas.getRequestSchema(operation)
900
- const requestBodySchemaNode = requestBodySchema ? convertSchema({ schema: requestBodySchema }, options) : undefined
699
+ const requestBodySchema = getRequestSchema(document, operation, { contentType: ctx.contentType })
700
+ const requestBodySchemaNode = requestBodySchema ? parseSchema({ schema: requestBodySchema }, options) : undefined
901
701
 
902
702
  const requestBodyDescription =
903
703
  operation.schema.requestBody && !isReference(operation.schema.requestBody)
@@ -920,12 +720,12 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
920
720
 
921
721
  const responses: Array<ResponseNode> = operation.getResponseStatusCodes().map((statusCode) => {
922
722
  const responseObj = operation.getResponseByStatusCode(statusCode)
923
- const responseSchema = oas.getResponseSchema(operation, statusCode)
723
+ const responseSchema = getResponseSchema(document, operation, statusCode, { contentType: ctx.contentType })
924
724
 
925
725
  const schema =
926
726
  responseSchema && Object.keys(responseSchema).length > 0
927
- ? convertSchema({ schema: responseSchema }, options)
928
- : createSchema({ type: resolveTypeOption(options.emptySchemaType) })
727
+ ? parseSchema({ schema: responseSchema }, options)
728
+ : createSchema({ type: typeOptionMap.get(options.emptySchemaType)! })
929
729
 
930
730
  const description = typeof responseObj === 'object' && responseObj !== null && !Array.isArray(responseObj) ? responseObj.description : undefined
931
731
 
@@ -934,7 +734,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
934
734
  ? (responseObj as { content?: Record<string, unknown> }).content
935
735
  : undefined
936
736
 
937
- const mediaType = rawContent ? toMediaType(Object.keys(rawContent)[0] ?? '') : toMediaType(operation.contentType ?? '')
737
+ const mediaType = rawContent ? getMediaType(Object.keys(rawContent)[0] ?? '') : getMediaType(operation.contentType ?? '')
938
738
 
939
739
  const keysToOmit = responseSchema?.properties
940
740
  ? Object.entries(responseSchema.properties)
@@ -965,38 +765,57 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
965
765
  })
966
766
  }
967
767
 
968
- /**
969
- * Converts an OpenAPI/Swagger spec (wrapped in a Kubb `Oas` instance) into
970
- * a `RootNode` — the top-level node of the `@kubb/ast` tree.
971
- */
972
- function parse<TOptions extends Partial<ParserOptions> = object>(options?: TOptions): RootNode {
973
- const mergedOptions: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...options }
768
+ return { parseSchema, parseOperation, parseParameter }
769
+ }
974
770
 
975
- const schemas: Array<SchemaNode> = Object.entries(schemaObjects).map(([name, schemaObject]) =>
976
- convertSchema({ schema: schemaObject as SchemaObject, name }, mergedOptions),
977
- )
771
+ /**
772
+ * Converts a single `SchemaObject` into a `SchemaNode`.
773
+ *
774
+ * @example
775
+ * ```ts
776
+ * const ctx = { document }
777
+ * parseSchema(ctx, { schema: { type: 'string', format: 'uuid' } })
778
+ * ```
779
+ */
780
+ export function parseSchema(ctx: OasParserContext, { schema, name }: { schema: SchemaObject; name?: string }, options?: Partial<ParserOptions>): SchemaNode {
781
+ return createSchemaParser(ctx).parseSchema({ schema, name }, options)
782
+ }
783
+
784
+ /**
785
+ * Converts the entire OpenAPI spec into a `RootNode` (the top-level `@kubb/ast` tree).
786
+ *
787
+ * This is the main entry point: `OpenAPI / Swagger → Kubb AST`.
788
+ * No code is generated here — the resulting tree is spec-agnostic and consumed by
789
+ * downstream plugins (`plugin-ts`, `plugin-zod`, …).
790
+ *
791
+ * @example
792
+ * ```ts
793
+ * const document = await parseFromConfig(config)
794
+ * const root = parseOas(document, { dateType: 'date', contentType: 'application/json' })
795
+ * ```
796
+ */
797
+ export function parseOas(
798
+ document: Document,
799
+ options: Partial<ParserOptions> & { contentType?: contentType } = {},
800
+ ): { root: RootNode; nameMapping: Map<string, string> } {
801
+ const { contentType, ...parserOptions } = options
802
+ const mergedOptions: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...parserOptions }
978
803
 
979
- const paths = oas.getPaths()
804
+ const { schemas: schemaObjects, nameMapping } = getSchemas(document, { contentType })
805
+ const { parseSchema: _parseSchema, parseOperation: _parseOperation } = createSchemaParser({ document, contentType })
980
806
 
981
- const operations: Array<OperationNode> = Object.entries(paths).flatMap(([_path, methods]) =>
982
- Object.entries(methods)
983
- .map(([, operation]) => (operation ? parseOperation(mergedOptions, oas, operation) : null))
984
- .filter((op): op is OperationNode => op !== null),
985
- )
807
+ const schemas: Array<SchemaNode> = Object.entries(schemaObjects).map(([name, schema]) => _parseSchema({ schema, name }, mergedOptions))
986
808
 
987
- return createRoot({ schemas, operations })
988
- }
809
+ const baseOas = new BaseOas(document)
810
+ const paths = baseOas.getPaths()
811
+
812
+ const operations: Array<OperationNode> = Object.entries(paths).flatMap(([_path, methods]) =>
813
+ Object.entries(methods)
814
+ .map(([, operation]) => (operation ? _parseOperation(mergedOptions, operation) : null))
815
+ .filter((op): op is OperationNode => op !== null),
816
+ )
817
+
818
+ const root = createRoot({ schemas, operations })
989
819
 
990
- const resolveRefs = (
991
- node: SchemaNode,
992
- resolveName: (ref: string) => string | undefined,
993
- resolveEnumName?: (name: string) => string | undefined,
994
- ): SchemaNode => resolveRefsNode({ node, nameMapping, resolveName, resolveEnumName })
995
-
996
- return {
997
- parse,
998
- convertSchema,
999
- resolveRefs,
1000
- nameMapping,
1001
- } as OasParser
820
+ return { root, nameMapping }
1002
821
  }