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