@kubb/adapter-oas 5.0.0-beta.6 → 5.0.0-beta.60

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,18 +1,19 @@
1
- import { URLPath } from '@internals/utils'
1
+ import { pascalCase } from '@internals/utils'
2
+ import { childName, enumPropName, extractRefName } from '@kubb/ast/utils'
2
3
  import { ast } from '@kubb/core'
3
- import BaseOas from 'oas'
4
4
  import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, SCHEMA_REF_PREFIX, typeOptionMap } from './constants.ts'
5
- import { isDiscriminator, isNullable, isReference } from './guards.ts'
6
- import { resolveRef } from './refs.ts'
5
+ import { oasDialect, type OasDialect } from './dialect.ts'
6
+ import { createDiscriminantNode, findDiscriminator } from './discriminator.ts'
7
+ import { getOperationId, getOperations, getRequestContentType, getResponseByStatusCode, getResponseStatusCodes } from './operation.ts'
7
8
  import {
8
9
  buildSchemaNode,
9
10
  flattenSchema,
10
11
  getDateType,
11
- getMediaType,
12
12
  getParameters,
13
13
  getPrimitiveType,
14
14
  getRequestBodyContentTypes,
15
15
  getRequestSchema,
16
+ getResponseBodyContentTypes,
16
17
  getResponseSchema,
17
18
  getSchemas,
18
19
  getSchemaType,
@@ -30,6 +31,16 @@ export type OasParserContext = {
30
31
  contentType?: ContentType
31
32
  }
32
33
 
34
+ /**
35
+ * The object returned by {@link createSchemaParser}.
36
+ * Contains parser functions bound to a specific document.
37
+ */
38
+ export type SchemaParser = {
39
+ parseSchema: (entry: { schema: SchemaObject; name?: string | null }, options?: Partial<ast.ParserOptions>) => ast.SchemaNode
40
+ parseOperation: (options: ast.ParserOptions, operation: Operation) => ast.OperationNode
41
+ parseParameter: (options: ast.ParserOptions, param: Record<string, unknown>) => ast.ParameterNode
42
+ }
43
+
33
44
  /**
34
45
  * Pre-computed per-schema context passed to every schema converter.
35
46
  *
@@ -50,13 +61,33 @@ type SchemaContext = {
50
61
  options: ast.ParserOptions
51
62
  }
52
63
 
64
+ /**
65
+ * One entry in the ordered schema rule table: a predicate paired with a converter. A rule whose
66
+ * `match` returns `true` may still `convert` to `null` to defer to the next rule (e.g. a `format`
67
+ * that is not convertible falls through to plain `type` handling).
68
+ */
69
+ type SchemaRule = {
70
+ /**
71
+ * Identifies the rule when reading the table or debugging which branch ran.
72
+ */
73
+ name: string
74
+ /**
75
+ * Returns `true` when this rule is responsible for the given context.
76
+ */
77
+ match: (context: SchemaContext) => boolean
78
+ /**
79
+ * Produces a node for the context, or `null` to fall through to the next rule.
80
+ */
81
+ convert: (context: SchemaContext) => ast.SchemaNode | null
82
+ }
83
+
53
84
  /**
54
85
  * Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
55
86
  *
56
87
  * This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
57
- * from the array to its items sub-schema, making them valid for downstream processing.
88
+ * from the array to its items sub-schema, so they are valid for downstream processing.
58
89
  *
59
- * @note This is a defensive measure for robustness with non-compliant specs.
90
+ * @note A defensive measure for non-compliant specs.
60
91
  */
61
92
  function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
62
93
  const isItemsObject = typeof schema.items === 'object' && !Array.isArray(schema.items)
@@ -74,14 +105,14 @@ function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
74
105
  *
75
106
  * Returns closures that share mutable state (`resolvingRefs` set for cycle detection).
76
107
  * Each converter branch (`convertRef`, `convertAllOf`, etc.) mutually recursively calls `parseSchema`,
77
- * made possible by hoisting of function declarations.
108
+ * which works because function declarations hoist.
78
109
  *
79
- * @note Not exported; called internally by `parseOas()` and `parseSchema()`.
110
+ * @internal
80
111
  */
81
- function createSchemaParser(ctx: OasParserContext) {
112
+ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect = oasDialect) {
82
113
  const document = ctx.document
83
114
 
84
- // Branch handlers each converts one OAS schema pattern to a SchemaNode.
115
+ // Branch handlers, each converts one OAS schema pattern to a SchemaNode.
85
116
 
86
117
  /**
87
118
  * Tracks `$ref` paths that are currently being resolved to prevent infinite
@@ -89,6 +120,16 @@ function createSchemaParser(ctx: OasParserContext) {
89
120
  */
90
121
  const resolvingRefs = new Set<string>()
91
122
 
123
+ /**
124
+ * Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
125
+ *
126
+ * Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
127
+ * it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
128
+ * since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
129
+ * Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
130
+ */
131
+ const resolvedRefCache = new Map<string, ast.SchemaNode | null>()
132
+
92
133
  /**
93
134
  * Converts a `$ref` schema into a `RefSchemaNode`.
94
135
  *
@@ -98,25 +139,29 @@ function createSchemaParser(ctx: OasParserContext) {
98
139
  * Circular refs are detected via `resolvingRefs` and leave `schema` as `undefined`.
99
140
  */
100
141
  function convertRef({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode {
101
- let resolvedSchema: ast.SchemaNode | undefined
142
+ let resolvedSchema: ast.SchemaNode | null = null
102
143
  const refPath = schema.$ref
103
144
  if (refPath && !resolvingRefs.has(refPath)) {
104
- try {
105
- const referenced = resolveRef<SchemaObject>(document, refPath)
106
- if (referenced) {
107
- resolvingRefs.add(refPath)
108
- resolvedSchema = parseSchema({ schema: referenced }, rawOptions)
109
- resolvingRefs.delete(refPath)
145
+ if (!resolvedRefCache.has(refPath)) {
146
+ try {
147
+ const referenced = dialect.resolveRef<SchemaObject>(document, refPath)
148
+ if (referenced) {
149
+ resolvingRefs.add(refPath)
150
+ resolvedSchema = parseSchema({ schema: referenced }, rawOptions)
151
+ resolvingRefs.delete(refPath)
152
+ }
153
+ } catch {
154
+ // Ref cannot be resolved in this document (e.g. unit tests with minimal documents).
110
155
  }
111
- } catch {
112
- // Ref cannot be resolved in this document (e.g. unit tests with minimal documents).
156
+ resolvedRefCache.set(refPath, resolvedSchema)
113
157
  }
158
+ resolvedSchema = resolvedRefCache.get(refPath) ?? null
114
159
  }
115
160
 
116
- return ast.createSchema({
161
+ return ast.factory.createSchema({
117
162
  ...buildSchemaNode(schema, name, nullable, defaultValue),
118
163
  type: 'ref',
119
- name: ast.extractRefName(schema.$ref!),
164
+ name: extractRefName(schema.$ref!),
120
165
  ref: schema.$ref,
121
166
  schema: resolvedSchema,
122
167
  })
@@ -133,12 +178,12 @@ function createSchemaParser(ctx: OasParserContext) {
133
178
  schema.additionalProperties === undefined
134
179
  ) {
135
180
  const [memberSchema] = schema.allOf as Array<SchemaObject | ReferenceObject>
136
- const memberNode = parseSchema({ schema: memberSchema! as SchemaObject, name: null }, rawOptions)
181
+ const memberNode = parseSchema({ schema: memberSchema! as SchemaObject, name }, rawOptions)
137
182
  const { kind: _kind, ...memberNodeProps } = memberNode
138
183
  const mergedNullable = nullable || memberNode.nullable || undefined
139
184
  const mergedDefault = schema.default === null && mergedNullable ? undefined : (schema.default ?? memberNode.default)
140
185
 
141
- return ast.createSchema({
186
+ return ast.factory.createSchema({
142
187
  ...memberNodeProps,
143
188
  name,
144
189
  title: schema.title ?? memberNode.title,
@@ -150,6 +195,7 @@ function createSchemaParser(ctx: OasParserContext) {
150
195
  default: mergedDefault,
151
196
  example: schema.example ?? memberNode.example,
152
197
  pattern: schema.pattern ?? ('pattern' in memberNode ? memberNode.pattern : undefined),
198
+ format: schema.format ?? memberNode.format,
153
199
  } as ast.DistributiveOmit<ast.SchemaNode, 'kind'>)
154
200
  }
155
201
 
@@ -159,16 +205,16 @@ function createSchemaParser(ctx: OasParserContext) {
159
205
  }> = []
160
206
  const allOfMembers: Array<ast.SchemaNode> = (schema.allOf as Array<SchemaObject | ReferenceObject>)
161
207
  .filter((item) => {
162
- if (!isReference(item) || !name) return true
163
- const deref = resolveRef<SchemaObject>(document, item.$ref)
164
- if (!deref || !isDiscriminator(deref)) return true
208
+ if (!dialect.isReference(item) || !name) return true
209
+ const deref = dialect.resolveRef<SchemaObject>(document, item.$ref)
210
+ if (!deref || !dialect.isDiscriminator(deref)) return true
165
211
  const parentUnion = deref.oneOf ?? deref.anyOf
166
212
  if (!parentUnion) return true
167
213
  const childRef = `${SCHEMA_REF_PREFIX}${name}`
168
- const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef)
214
+ const inOneOf = parentUnion.some((oneOfItem) => dialect.isReference(oneOfItem) && oneOfItem.$ref === childRef)
169
215
  const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef)
170
216
  if (inOneOf || inMapping) {
171
- const discriminatorValue = ast.findDiscriminator(deref.discriminator.mapping, childRef)
217
+ const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef)
172
218
  if (discriminatorValue) {
173
219
  filteredDiscriminantValues.push({
174
220
  propertyName: deref.discriminator.propertyName,
@@ -179,7 +225,7 @@ function createSchemaParser(ctx: OasParserContext) {
179
225
  }
180
226
  return true
181
227
  })
182
- .map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))
228
+ .map((s) => parseSchema({ schema: s as SchemaObject, name }, rawOptions))
183
229
 
184
230
  const syntheticStart = allOfMembers.length
185
231
 
@@ -189,9 +235,9 @@ function createSchemaParser(ctx: OasParserContext) {
189
235
 
190
236
  if (missingRequired.length) {
191
237
  const resolvedMembers = (schema.allOf as Array<SchemaObject | ReferenceObject>).flatMap((item) => {
192
- if (!isReference(item)) return [item as SchemaObject]
193
- const deref = resolveRef<SchemaObject>(document, item.$ref)
194
- return deref && !isReference(deref) ? [deref] : []
238
+ if (!dialect.isReference(item)) return [item as SchemaObject]
239
+ const deref = dialect.resolveRef<SchemaObject>(document, item.$ref)
240
+ return deref && !dialect.isReference(deref) ? [deref] : []
195
241
  })
196
242
 
197
243
  for (const key of missingRequired) {
@@ -204,6 +250,7 @@ function createSchemaParser(ctx: OasParserContext) {
204
250
  properties: { [key]: resolved.properties[key] },
205
251
  required: [key],
206
252
  } as SchemaObject,
253
+ name,
207
254
  },
208
255
  rawOptions,
209
256
  ),
@@ -217,16 +264,19 @@ function createSchemaParser(ctx: OasParserContext) {
217
264
 
218
265
  if (schema.properties) {
219
266
  const { allOf: _allOf, ...schemaWithoutAllOf } = schema
267
+ // Don't pass `name` here, the result must stay anonymous so it can be merged with the
268
+ // adjacent synthetic object in `mergeAdjacentObjectsLazy`. Nested enum qualification
269
+ // happens upstream via `convertObject`'s `setEnumName` propagation.
220
270
  allOfMembers.push(parseSchema({ schema: schemaWithoutAllOf }, rawOptions))
221
271
  }
222
272
 
223
273
  for (const { propertyName, value } of filteredDiscriminantValues) {
224
- allOfMembers.push(ast.createDiscriminantNode({ propertyName, value }))
274
+ allOfMembers.push(createDiscriminantNode({ propertyName, value }))
225
275
  }
226
276
 
227
- return ast.createSchema({
277
+ return ast.factory.createSchema({
228
278
  type: 'intersection',
229
- members: [...ast.mergeAdjacentObjects(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjects(allOfMembers.slice(syntheticStart))],
279
+ members: [...ast.mergeAdjacentObjectsLazy(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjectsLazy(allOfMembers.slice(syntheticStart))],
230
280
  ...buildSchemaNode(schema, name, nullable, defaultValue),
231
281
  })
232
282
  }
@@ -243,7 +293,7 @@ function createSchemaParser(ctx: OasParserContext) {
243
293
  return null
244
294
  }
245
295
 
246
- return ast.createSchema({
296
+ return ast.factory.createSchema({
247
297
  type: 'object',
248
298
  primitive: 'object',
249
299
  properties: [discriminatorProperty],
@@ -254,10 +304,10 @@ function createSchemaParser(ctx: OasParserContext) {
254
304
  const strategy: 'one' | 'any' = schema.oneOf ? 'one' : 'any'
255
305
  const unionBase = {
256
306
  ...buildSchemaNode(schema, name, nullable, defaultValue),
257
- discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
307
+ discriminatorPropertyName: dialect.isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
258
308
  strategy,
259
309
  }
260
- const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
310
+ const discriminator = dialect.isDiscriminator(schema) ? schema.discriminator : undefined
261
311
  const sharedPropertiesNode = schema.properties
262
312
  ? (() => {
263
313
  const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
@@ -270,9 +320,9 @@ function createSchemaParser(ctx: OasParserContext) {
270
320
 
271
321
  if (sharedPropertiesNode || discriminator?.mapping) {
272
322
  const members = unionMembers.map((s) => {
273
- const ref = isReference(s) ? s.$ref : undefined
274
- const discriminatorValue = ast.findDiscriminator(discriminator?.mapping, ref)
275
- const memberNode = parseSchema({ schema: s as SchemaObject }, rawOptions)
323
+ const ref = dialect.isReference(s) ? s.$ref : undefined
324
+ const discriminatorValue = findDiscriminator(discriminator?.mapping, ref)
325
+ const memberNode = parseSchema({ schema: s as SchemaObject, name }, rawOptions)
276
326
 
277
327
  if (!discriminatorValue || !discriminator) {
278
328
  return memberNode
@@ -289,12 +339,12 @@ function createSchemaParser(ctx: OasParserContext) {
289
339
  )
290
340
  : undefined
291
341
 
292
- return ast.createSchema({
342
+ return ast.factory.createSchema({
293
343
  type: 'intersection',
294
344
  members: [
295
345
  memberNode,
296
346
  narrowedDiscriminatorNode ??
297
- ast.createDiscriminantNode({
347
+ createDiscriminantNode({
298
348
  propertyName: discriminator.propertyName,
299
349
  value: discriminatorValue,
300
350
  }),
@@ -302,7 +352,7 @@ function createSchemaParser(ctx: OasParserContext) {
302
352
  })
303
353
  })
304
354
 
305
- const unionNode = ast.createSchema({
355
+ const unionNode = ast.factory.createSchema({
306
356
  type: 'union',
307
357
  ...unionBase,
308
358
  members,
@@ -312,17 +362,17 @@ function createSchemaParser(ctx: OasParserContext) {
312
362
  return unionNode
313
363
  }
314
364
 
315
- return ast.createSchema({
365
+ return ast.factory.createSchema({
316
366
  type: 'intersection',
317
367
  ...buildSchemaNode(schema, name, nullable, defaultValue),
318
368
  members: [unionNode, sharedPropertiesNode],
319
369
  })
320
370
  }
321
371
 
322
- return ast.createSchema({
372
+ return ast.factory.createSchema({
323
373
  type: 'union',
324
374
  ...unionBase,
325
- members: ast.simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))),
375
+ members: ast.simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s as SchemaObject, name }, rawOptions))),
326
376
  })
327
377
  }
328
378
 
@@ -333,18 +383,19 @@ function createSchemaParser(ctx: OasParserContext) {
333
383
  const constValue = schema.const
334
384
 
335
385
  if (constValue === null) {
336
- return ast.createSchema({
386
+ return ast.factory.createSchema({
337
387
  type: 'null',
338
388
  primitive: 'null',
339
389
  name,
340
390
  title: schema.title,
341
391
  description: schema.description,
342
392
  deprecated: schema.deprecated,
393
+ format: schema.format,
343
394
  })
344
395
  }
345
396
 
346
397
  const constPrimitive = getPrimitiveType(typeof constValue === 'number' ? 'number' : typeof constValue === 'boolean' ? 'boolean' : 'string')
347
- return ast.createSchema({
398
+ return ast.factory.createSchema({
348
399
  type: 'enum',
349
400
  primitive: constPrimitive,
350
401
  enumValues: [constValue as string | number | boolean],
@@ -360,7 +411,7 @@ function createSchemaParser(ctx: OasParserContext) {
360
411
  const base = buildSchemaNode(schema, name, nullable, defaultValue)
361
412
 
362
413
  if (schema.format === 'int64') {
363
- return ast.createSchema({
414
+ return ast.factory.createSchema({
364
415
  type: options.integerType === 'bigint' ? 'bigint' : 'integer',
365
416
  primitive: 'integer',
366
417
  ...base,
@@ -376,7 +427,7 @@ function createSchemaParser(ctx: OasParserContext) {
376
427
  if (!dateType) return null
377
428
 
378
429
  if (dateType.type === 'datetime') {
379
- return ast.createSchema({
430
+ return ast.factory.createSchema({
380
431
  ...base,
381
432
  primitive: 'string' as const,
382
433
  type: 'datetime',
@@ -384,7 +435,7 @@ function createSchemaParser(ctx: OasParserContext) {
384
435
  local: dateType.local,
385
436
  })
386
437
  }
387
- return ast.createSchema({
438
+ return ast.factory.createSchema({
388
439
  ...base,
389
440
  primitive: 'string' as const,
390
441
  type: dateType.type,
@@ -398,14 +449,14 @@ function createSchemaParser(ctx: OasParserContext) {
398
449
  const specialPrimitive: ast.PrimitiveSchemaType = specialType === 'number' || specialType === 'integer' || specialType === 'bigint' ? specialType : 'string'
399
450
 
400
451
  if (specialType === 'number' || specialType === 'integer' || specialType === 'bigint') {
401
- return ast.createSchema({
452
+ return ast.factory.createSchema({
402
453
  ...base,
403
454
  primitive: specialPrimitive,
404
455
  type: specialType,
405
456
  })
406
457
  }
407
458
  if (specialType === 'url') {
408
- return ast.createSchema({
459
+ return ast.factory.createSchema({
409
460
  ...base,
410
461
  primitive: 'string' as const,
411
462
  type: 'url',
@@ -414,21 +465,21 @@ function createSchemaParser(ctx: OasParserContext) {
414
465
  })
415
466
  }
416
467
  if (specialType === 'ipv4') {
417
- return ast.createSchema({
468
+ return ast.factory.createSchema({
418
469
  ...base,
419
470
  primitive: 'string' as const,
420
471
  type: 'ipv4',
421
472
  })
422
473
  }
423
474
  if (specialType === 'ipv6') {
424
- return ast.createSchema({
475
+ return ast.factory.createSchema({
425
476
  ...base,
426
477
  primitive: 'string' as const,
427
478
  type: 'ipv6',
428
479
  })
429
480
  }
430
481
  if (specialType === 'uuid' || specialType === 'email') {
431
- return ast.createSchema({
482
+ return ast.factory.createSchema({
432
483
  ...base,
433
484
  primitive: 'string' as const,
434
485
  type: specialType,
@@ -437,7 +488,7 @@ function createSchemaParser(ctx: OasParserContext) {
437
488
  })
438
489
  }
439
490
 
440
- return ast.createSchema({
491
+ return ast.factory.createSchema({
441
492
  ...base,
442
493
  primitive: specialPrimitive,
443
494
  type: specialType as ast.ScalarSchemaType,
@@ -454,6 +505,22 @@ function createSchemaParser(ctx: OasParserContext) {
454
505
 
455
506
  const nullInEnum = schema.enum!.includes(null)
456
507
  const filteredValues = (nullInEnum ? schema.enum!.filter((v) => v !== null) : schema.enum!) as Array<string | number | boolean>
508
+
509
+ // drf-spectacular `NullEnum` ({ enum: [null] }) is just `null`. An empty enum node would
510
+ // render as `never` (plugin-ts) / invalid `z.enum([])` (plugin-zod). Mirror the `const: null`
511
+ // branch so it renders as a clean `null` (not `z.null().nullable()`).
512
+ if (nullInEnum && filteredValues.length === 0) {
513
+ return ast.factory.createSchema({
514
+ type: 'null',
515
+ primitive: 'null',
516
+ name,
517
+ title: schema.title,
518
+ description: schema.description,
519
+ deprecated: schema.deprecated,
520
+ format: schema.format,
521
+ })
522
+ }
523
+
457
524
  const enumNullable = nullable || nullInEnum || undefined
458
525
  const enumDefault = schema.default === null && enumNullable ? undefined : schema.default
459
526
  const enumPrimitive = getPrimitiveType(type)
@@ -470,6 +537,7 @@ function createSchemaParser(ctx: OasParserContext) {
470
537
  writeOnly: schema.writeOnly,
471
538
  default: enumDefault,
472
539
  example: schema.example,
540
+ format: schema.format,
473
541
  }
474
542
 
475
543
  const extensionKey = enumExtensionKeys.find((key) => key in schema)
@@ -482,7 +550,7 @@ function createSchemaParser(ctx: OasParserContext) {
482
550
  const uniqueValues = [...new Set(filteredValues)]
483
551
  const seenNames = new Set<string>()
484
552
 
485
- return ast.createSchema({
553
+ return ast.factory.createSchema({
486
554
  ...enumBase,
487
555
  primitive: enumPrimitiveType,
488
556
  namedEnumValues: uniqueValues
@@ -499,7 +567,7 @@ function createSchemaParser(ctx: OasParserContext) {
499
567
  })
500
568
  }
501
569
 
502
- return ast.createSchema({
570
+ return ast.factory.createSchema({
503
571
  ...enumBase,
504
572
  enumValues: [...new Set(filteredValues)],
505
573
  })
@@ -513,21 +581,23 @@ function createSchemaParser(ctx: OasParserContext) {
513
581
  ? Object.entries(schema.properties).map(([propName, propSchema]) => {
514
582
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required
515
583
  const resolvedPropSchema = propSchema as SchemaObject
516
- const propNullable = isNullable(resolvedPropSchema)
584
+ const propNullable = dialect.isNullable(resolvedPropSchema)
517
585
 
518
- const resolvedChildName = ast.childName(name, propName)
586
+ const resolvedChildName = childName(name, propName)
519
587
  const propNode = parseSchema({ schema: resolvedPropSchema, name: resolvedChildName }, rawOptions)
520
- let schemaNode = ast.setEnumName(propNode, name, propName, options.enumSuffix)
521
-
522
- const tupleNode = ast.narrowSchema(schemaNode, 'tuple')
523
- if (tupleNode?.items) {
524
- const namedItems = tupleNode.items.map((item) => ast.setEnumName(item, name, propName, options.enumSuffix))
525
- if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
526
- schemaNode = { ...tupleNode, items: namedItems }
588
+ const schemaNode = (() => {
589
+ const node = ast.setEnumName(propNode, name, propName, options.enumSuffix)
590
+ const tupleNode = ast.narrowSchema(node, 'tuple')
591
+ if (tupleNode?.items) {
592
+ const namedItems = tupleNode.items.map((item) => ast.setEnumName(item, name, propName, options.enumSuffix))
593
+ if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
594
+ return { ...tupleNode, items: namedItems }
595
+ }
527
596
  }
528
- }
597
+ return node
598
+ })()
529
599
 
530
- return ast.createProperty({
600
+ return ast.factory.createProperty({
531
601
  name: propName,
532
602
  schema: {
533
603
  ...schemaNode,
@@ -539,18 +609,15 @@ function createSchemaParser(ctx: OasParserContext) {
539
609
  : []
540
610
 
541
611
  const additionalProperties = schema.additionalProperties
542
- let additionalPropertiesNode: ast.SchemaNode | boolean | undefined
543
- if (additionalProperties === true) {
544
- additionalPropertiesNode = true
545
- } else if (additionalProperties && Object.keys(additionalProperties).length > 0) {
546
- additionalPropertiesNode = parseSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
547
- } else if (additionalProperties === false) {
548
- additionalPropertiesNode = false
549
- } else if (additionalProperties) {
550
- additionalPropertiesNode = ast.createSchema({
551
- type: typeOptionMap.get(options.unknownType)!,
552
- })
553
- }
612
+ const additionalPropertiesNode: ast.SchemaNode | boolean | undefined = (() => {
613
+ if (additionalProperties === true) return true
614
+ if (additionalProperties === false) return false
615
+ if (additionalProperties && Object.keys(additionalProperties).length > 0) {
616
+ return parseSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
617
+ }
618
+ if (additionalProperties) return ast.factory.createSchema({ type: typeOptionMap.get(options.unknownType)! })
619
+ return undefined
620
+ })()
554
621
 
555
622
  const rawPatternProperties = 'patternProperties' in schema ? schema.patternProperties : undefined
556
623
 
@@ -559,7 +626,7 @@ function createSchemaParser(ctx: OasParserContext) {
559
626
  Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [
560
627
  pattern,
561
628
  patternSchema === true || (typeof patternSchema === 'object' && Object.keys(patternSchema).length === 0)
562
- ? ast.createSchema({
629
+ ? ast.factory.createSchema({
563
630
  type: typeOptionMap.get(options.unknownType)!,
564
631
  })
565
632
  : parseSchema({ schema: patternSchema as SchemaObject }, rawOptions),
@@ -567,7 +634,7 @@ function createSchemaParser(ctx: OasParserContext) {
567
634
  )
568
635
  : undefined
569
636
 
570
- const objectNode: ast.SchemaNode = ast.createSchema({
637
+ const objectNode: ast.SchemaNode = ast.factory.createSchema({
571
638
  type: 'object',
572
639
  primitive: 'object',
573
640
  properties,
@@ -578,10 +645,10 @@ function createSchemaParser(ctx: OasParserContext) {
578
645
  ...buildSchemaNode(schema, name, nullable, defaultValue),
579
646
  })
580
647
 
581
- if (isDiscriminator(schema) && schema.discriminator.mapping) {
648
+ if (dialect.isDiscriminator(schema) && schema.discriminator.mapping) {
582
649
  const discPropName = schema.discriminator.propertyName
583
650
  const values = Object.keys(schema.discriminator.mapping)
584
- const enumName = name ? ast.enumPropName(name, discPropName, options.enumSuffix) : undefined
651
+ const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : undefined
585
652
  return ast.setDiscriminatorEnum({
586
653
  node: objectNode,
587
654
  propertyName: discPropName,
@@ -598,9 +665,9 @@ function createSchemaParser(ctx: OasParserContext) {
598
665
  */
599
666
  function convertTuple({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode {
600
667
  const tupleItems = (schema.prefixItems ?? []).map((item) => parseSchema({ schema: item as SchemaObject }, rawOptions))
601
- const rest = schema.items ? parseSchema({ schema: schema.items as SchemaObject }, rawOptions) : ast.createSchema({ type: 'any' })
668
+ const rest = schema.items ? parseSchema({ schema: schema.items as SchemaObject }, rawOptions) : ast.factory.createSchema({ type: 'any' })
602
669
 
603
- return ast.createSchema({
670
+ return ast.factory.createSchema({
604
671
  type: 'tuple',
605
672
  primitive: 'array',
606
673
  items: tupleItems,
@@ -616,10 +683,10 @@ function createSchemaParser(ctx: OasParserContext) {
616
683
  */
617
684
  function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): ast.SchemaNode {
618
685
  const rawItems = schema.items as SchemaObject | undefined
619
- const itemName = rawItems?.enum?.length && name ? ast.enumPropName(undefined, name, options.enumSuffix) : undefined
686
+ const itemName = rawItems?.enum?.length && name ? enumPropName(null, name, options.enumSuffix) : name
620
687
  const items = rawItems ? [parseSchema({ schema: rawItems, name: itemName }, rawOptions)] : []
621
688
 
622
- return ast.createSchema({
689
+ return ast.factory.createSchema({
623
690
  type: 'array',
624
691
  primitive: 'array',
625
692
  items,
@@ -634,7 +701,7 @@ function createSchemaParser(ctx: OasParserContext) {
634
701
  * Converts a `type: 'string'` schema into a `StringSchemaNode`.
635
702
  */
636
703
  function convertString({ schema, name, nullable, defaultValue }: SchemaContext): ast.SchemaNode {
637
- return ast.createSchema({
704
+ return ast.factory.createSchema({
638
705
  type: 'string',
639
706
  primitive: 'string',
640
707
  min: schema.minLength,
@@ -648,7 +715,7 @@ function createSchemaParser(ctx: OasParserContext) {
648
715
  * Converts a `type: 'number'` or `type: 'integer'` schema.
649
716
  */
650
717
  function convertNumeric({ schema, name, nullable, defaultValue }: SchemaContext, type: 'number' | 'integer'): ast.SchemaNode {
651
- return ast.createSchema({
718
+ return ast.factory.createSchema({
652
719
  type,
653
720
  primitive: type,
654
721
  min: schema.minimum,
@@ -664,7 +731,7 @@ function createSchemaParser(ctx: OasParserContext) {
664
731
  * Converts a `type: 'boolean'` schema.
665
732
  */
666
733
  function convertBoolean({ schema, name, nullable, defaultValue }: SchemaContext): ast.SchemaNode {
667
- return ast.createSchema({
734
+ return ast.factory.createSchema({
668
735
  type: 'boolean',
669
736
  primitive: 'boolean',
670
737
  ...buildSchemaNode(schema, name, nullable, defaultValue),
@@ -675,7 +742,7 @@ function createSchemaParser(ctx: OasParserContext) {
675
742
  * Converts an explicit `type: 'null'` schema.
676
743
  */
677
744
  function convertNull({ schema, name, nullable }: SchemaContext): ast.SchemaNode {
678
- return ast.createSchema({
745
+ return ast.factory.createSchema({
679
746
  type: 'null',
680
747
  primitive: 'null',
681
748
  name,
@@ -683,15 +750,90 @@ function createSchemaParser(ctx: OasParserContext) {
683
750
  description: schema.description,
684
751
  deprecated: schema.deprecated,
685
752
  nullable,
753
+ format: schema.format,
686
754
  })
687
755
  }
688
756
 
689
757
  /**
690
- * Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
758
+ * Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
759
+ * into a `blob` node.
760
+ */
761
+ function convertBlob({ schema, name, nullable, defaultValue }: SchemaContext): ast.SchemaNode {
762
+ return ast.factory.createSchema({
763
+ type: 'blob',
764
+ primitive: 'string',
765
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
766
+ })
767
+ }
768
+
769
+ /**
770
+ * Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
691
771
  *
692
- * Dispatch order (first match wins): `$ref` `allOf` `oneOf`/`anyOf` `const` `format`
693
- * octet-stream blob multi-type array constraint-inferred type `enum` object/array/tuple/scalar
694
- * → empty-schema fallback (`emptySchemaType` option).
772
+ * Returns `null` when only one non-`null` type remains (e.g. `['string', 'null']`), so `parseSchema`
773
+ * falls through and handles it as that single type with nullability already folded in.
774
+ */
775
+ function convertMultiType({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode | null {
776
+ const types = schema.type as Array<string>
777
+ const nonNullTypes = types.filter((t) => t !== 'null')
778
+ if (nonNullTypes.length <= 1) return null
779
+
780
+ const arrayNullable = types.includes('null') || nullable || undefined
781
+ return ast.factory.createSchema({
782
+ type: 'union',
783
+ members: nonNullTypes.map((t) => parseSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
784
+ ...buildSchemaNode(schema, name, arrayNullable, defaultValue),
785
+ })
786
+ }
787
+
788
+ /**
789
+ * Ordered schema rule table. Order is significant: composition keywords (`$ref`, `allOf`,
790
+ * `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
791
+ * `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
792
+ * match/convert/fall-through contract.
793
+ */
794
+ const schemaRules: Array<SchemaRule> = [
795
+ { name: 'ref', match: ({ schema }) => dialect.isReference(schema), convert: convertRef },
796
+ { name: 'allOf', match: ({ schema }) => !!schema.allOf?.length, convert: convertAllOf },
797
+ { name: 'union', match: ({ schema }) => !!(schema.oneOf?.length || schema.anyOf?.length), convert: convertUnion },
798
+ { name: 'const', match: ({ schema }) => 'const' in schema && schema.const !== undefined, convert: convertConst },
799
+ { name: 'format', match: ({ schema }) => !!schema.format, convert: convertFormat },
800
+ {
801
+ name: 'blob',
802
+ match: ({ schema }) => dialect.isBinary(schema),
803
+ convert: convertBlob,
804
+ },
805
+ { name: 'multi-type', match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1, convert: convertMultiType },
806
+ {
807
+ name: 'constrained-string',
808
+ match: ({ schema, type }) => !type && (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined),
809
+ convert: convertString,
810
+ },
811
+ {
812
+ name: 'constrained-number',
813
+ match: ({ schema, type }) => !type && (schema.minimum !== undefined || schema.maximum !== undefined),
814
+ convert: (ctx) => convertNumeric(ctx, 'number'),
815
+ },
816
+ { name: 'enum', match: ({ schema }) => !!schema.enum?.length, convert: convertEnum },
817
+ {
818
+ name: 'object',
819
+ match: ({ schema, type }) => type === 'object' || !!schema.properties || !!schema.additionalProperties || 'patternProperties' in schema,
820
+ convert: convertObject,
821
+ },
822
+ { name: 'tuple', match: ({ schema }) => 'prefixItems' in schema, convert: convertTuple },
823
+ { name: 'array', match: ({ schema, type }) => type === 'array' || 'items' in schema, convert: convertArray },
824
+ { name: 'string', match: ({ type }) => type === 'string', convert: convertString },
825
+ { name: 'number', match: ({ type }) => type === 'number', convert: (ctx) => convertNumeric(ctx, 'number') },
826
+ { name: 'integer', match: ({ type }) => type === 'integer', convert: (ctx) => convertNumeric(ctx, 'integer') },
827
+ { name: 'boolean', match: ({ type }) => type === 'boolean', convert: convertBoolean },
828
+ { name: 'null', match: ({ type }) => type === 'null', convert: convertNull },
829
+ ]
830
+
831
+ /**
832
+ * Converts an OAS `SchemaObject` into a `SchemaNode`.
833
+ *
834
+ * Builds the per-schema context, then walks the ordered {@link schemaRules} table and returns
835
+ * the first converter that produces a node. When none match, falls back to the configured
836
+ * `emptySchemaType`.
695
837
  */
696
838
  function parseSchema({ schema, name }: { schema: SchemaObject; name?: string | null }, rawOptions?: Partial<ast.ParserOptions>): ast.SchemaNode {
697
839
  const options: ast.ParserOptions = {
@@ -703,11 +845,11 @@ function createSchemaParser(ctx: OasParserContext) {
703
845
  return parseSchema({ schema: flattenedSchema, name }, rawOptions)
704
846
  }
705
847
 
706
- const nullable = isNullable(schema) || undefined
848
+ const nullable = dialect.isNullable(schema) || undefined
707
849
  const defaultValue = schema.default === null && nullable ? undefined : schema.default
708
850
  const type = Array.isArray(schema.type) ? schema.type[0] : schema.type
709
851
 
710
- const ctx: SchemaContext = {
852
+ const schemaCtx: SchemaContext = {
711
853
  schema,
712
854
  name,
713
855
  nullable,
@@ -717,80 +859,36 @@ function createSchemaParser(ctx: OasParserContext) {
717
859
  options,
718
860
  }
719
861
 
720
- if (isReference(schema)) return convertRef(ctx)
721
-
722
- if (schema.allOf?.length) return convertAllOf(ctx)
723
- const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
724
- if (unionMembers.length) return convertUnion(ctx)
725
-
726
- if ('const' in schema && schema.const !== undefined) return convertConst(ctx)
727
-
728
- if (schema.format) {
729
- const formatResult = convertFormat(ctx)
730
- if (formatResult) return formatResult
862
+ for (const rule of schemaRules) {
863
+ if (!rule.match(schemaCtx)) continue
864
+ const node = rule.convert(schemaCtx)
865
+ if (node) return node
731
866
  }
732
867
 
733
- if (schema.type === 'string' && schema.contentMediaType === 'application/octet-stream') {
734
- return ast.createSchema({
735
- type: 'blob',
736
- primitive: 'string',
737
- ...buildSchemaNode(schema, name, nullable, defaultValue),
738
- })
739
- }
740
-
741
- if (Array.isArray(schema.type) && schema.type.length > 1) {
742
- const nonNullTypes = schema.type.filter((t) => t !== 'null') as string[]
743
- const arrayNullable = schema.type.includes('null') || nullable || undefined
744
-
745
- if (nonNullTypes.length > 1) {
746
- return ast.createSchema({
747
- type: 'union',
748
- members: nonNullTypes.map((t) => parseSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
749
- ...buildSchemaNode(schema, name, arrayNullable, defaultValue),
750
- })
751
- }
752
- }
753
-
754
- if (!type) {
755
- if (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined) {
756
- return convertString(ctx)
757
- }
758
- if (schema.minimum !== undefined || schema.maximum !== undefined) {
759
- return convertNumeric(ctx, 'number')
760
- }
761
- }
762
-
763
- if (schema.enum?.length) return convertEnum(ctx)
764
- if (type === 'object' || schema.properties || schema.additionalProperties || 'patternProperties' in schema) return convertObject(ctx)
765
- if ('prefixItems' in schema) return convertTuple(ctx)
766
- if (type === 'array' || 'items' in schema) return convertArray(ctx)
767
- if (type === 'string') return convertString(ctx)
768
- if (type === 'number') return convertNumeric(ctx, 'number')
769
- if (type === 'integer') return convertNumeric(ctx, 'integer')
770
- if (type === 'boolean') return convertBoolean(ctx)
771
- if (type === 'null') return convertNull(ctx)
772
-
773
868
  const emptyType = typeOptionMap.get(options.emptySchemaType)!
774
- return ast.createSchema({
869
+ return ast.factory.createSchema({
775
870
  type: emptyType as ast.ScalarSchemaType,
776
871
  name,
777
872
  title: schema.title,
778
873
  description: schema.description,
874
+ format: schema.format,
779
875
  })
780
876
  }
781
877
 
782
878
  /**
783
879
  * Converts a dereferenced OAS parameter object into a `ParameterNode`.
784
880
  */
785
- function parseParameter(options: ast.ParserOptions, param: Record<string, unknown>): ast.ParameterNode {
881
+ function parseParameter(options: ast.ParserOptions, param: Record<string, unknown>, parentName?: string): ast.ParameterNode {
786
882
  const required = (param['required'] as boolean | undefined) ?? false
883
+ const paramName = param['name'] as string
884
+ const schemaName = parentName && paramName ? pascalCase(`${parentName} ${paramName}`) : undefined
787
885
 
788
886
  const schema: ast.SchemaNode = param['schema']
789
- ? parseSchema({ schema: param['schema'] as SchemaObject }, options)
790
- : ast.createSchema({ type: typeOptionMap.get(options.unknownType)! })
887
+ ? parseSchema({ schema: param['schema'] as SchemaObject, name: schemaName }, options)
888
+ : ast.factory.createSchema({ type: typeOptionMap.get(options.unknownType)! })
791
889
 
792
- return ast.createParameter({
793
- name: param['name'] as string,
890
+ return ast.factory.createParameter({
891
+ name: paramName,
794
892
  in: param['in'] as ast.ParameterLocation,
795
893
  schema: {
796
894
  ...schema,
@@ -839,25 +937,27 @@ function createSchemaParser(ctx: OasParserContext) {
839
937
  * Collects property names whose schema has a truthy boolean flag (`readOnly` or `writeOnly`).
840
938
  * `$ref` entries are skipped since their flags live on the dereferenced target.
841
939
  */
842
- function collectPropertyKeysByFlag(schema: SchemaObject | null, flag: 'readOnly' | 'writeOnly'): string[] | undefined {
843
- if (!schema?.properties) return undefined
940
+ function collectPropertyKeysByFlag(schema: SchemaObject | null, flag: 'readOnly' | 'writeOnly'): Array<string> | null {
941
+ if (!schema?.properties) return null
844
942
 
845
- const keys: string[] = []
943
+ const keys: Array<string> = []
846
944
  for (const key in schema.properties) {
847
945
  const prop = schema.properties[key]
848
- if (prop && !isReference(prop) && (prop as Record<string, unknown>)[flag]) {
946
+ if (prop && !dialect.isReference(prop) && (prop as Record<string, unknown>)[flag]) {
849
947
  keys.push(key)
850
948
  }
851
949
  }
852
- return keys.length ? keys : undefined
950
+ return keys.length ? keys : null
853
951
  }
854
952
 
855
953
  /**
856
954
  * Converts an OAS `Operation` into an `OperationNode`.
857
955
  */
858
956
  function parseOperation(options: ast.ParserOptions, operation: Operation): ast.OperationNode {
957
+ const operationId = getOperationId(operation)
958
+ const operationName = operationId ? pascalCase(operationId) : undefined
859
959
  const parameters: Array<ast.ParameterNode> = getParameters(document, operation).map((param) =>
860
- parseParameter(options, param as unknown as Record<string, unknown>),
960
+ parseParameter(options, param as unknown as Record<string, unknown>, operationName),
861
961
  )
862
962
 
863
963
  // Determine which content types to include in requestBody.content.
@@ -866,6 +966,7 @@ function createSchemaParser(ctx: OasParserContext) {
866
966
  const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation)
867
967
 
868
968
  const requestBodyMeta = getRequestBodyMeta(operation)
969
+ const requestBodyName = operationName ? `${operationName}Request` : undefined
869
970
 
870
971
  const content = allContentTypes.flatMap((ct) => {
871
972
  const schema = getRequestSchema(document, operation, { contentType: ct })
@@ -873,7 +974,7 @@ function createSchemaParser(ctx: OasParserContext) {
873
974
  return [
874
975
  {
875
976
  contentType: ct,
876
- schema: ast.syncOptionality(parseSchema({ schema }, options), requestBodyMeta.required),
977
+ schema: ast.syncOptionality(parseSchema({ schema, name: requestBodyName }, options), requestBodyMeta.required),
877
978
  keysToOmit: collectPropertyKeysByFlag(schema, 'readOnly'),
878
979
  },
879
980
  ]
@@ -888,39 +989,60 @@ function createSchemaParser(ctx: OasParserContext) {
888
989
  }
889
990
  : undefined
890
991
 
891
- const responses: Array<ast.ResponseNode> = operation.getResponseStatusCodes().map((statusCode) => {
892
- const responseObj = operation.getResponseByStatusCode(statusCode)
893
- const responseSchema = getResponseSchema(document, operation, statusCode, { contentType: ctx.contentType })
992
+ const responses: Array<ast.ResponseNode> = getResponseStatusCodes(operation).map((statusCode) => {
993
+ const responseObj = getResponseByStatusCode({ document, operation, statusCode })
994
+
995
+ // Use `Status<code>` (matching plugin-ts's resolveResponseStatusName convention) so the
996
+ // qualified names for nested enums don't collide with top-level component schemas that
997
+ // happen to be named `<operation><statusCode>` (e.g. `GetMaintenance200`).
998
+ const responseName = operationName ? `${operationName}Status${statusCode}` : undefined
999
+ const { description } = getResponseMeta(responseObj)
1000
+
1001
+ const parseEntrySchema = (contentType?: string) => {
1002
+ const raw = getResponseSchema(document, operation, statusCode, { contentType })
1003
+ const node =
1004
+ raw && Object.keys(raw).length > 0
1005
+ ? parseSchema({ schema: raw, name: responseName }, options)
1006
+ : ast.factory.createSchema({ type: typeOptionMap.get(options.emptySchemaType)! })
1007
+ return { schema: node, keysToOmit: collectPropertyKeysByFlag(raw, 'writeOnly') }
1008
+ }
894
1009
 
895
- const schema =
896
- responseSchema && Object.keys(responseSchema).length > 0
897
- ? parseSchema({ schema: responseSchema }, options)
898
- : ast.createSchema({
899
- type: typeOptionMap.get(options.emptySchemaType)!,
900
- })
1010
+ // Build one entry per declared response content type so plugins can union the variants.
1011
+ // When a global contentType is configured, restrict to that single type (mirrors requestBody).
1012
+ const responseContentTypes = ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(document, operation, statusCode)
1013
+ const content = responseContentTypes.map((contentType) => ({ contentType, ...parseEntrySchema(contentType) }))
901
1014
 
902
- const { description, content } = getResponseMeta(responseObj)
903
- const mediaType = content ? getMediaType(Object.keys(content)[0] ?? '') : getMediaType(operation.contentType ?? '')
1015
+ // Body-less responses keep a single fallback entry so the response still resolves to a
1016
+ // (void/any) schema, matching how `requestBody` only carries schemas inside `content`.
1017
+ if (content.length === 0) {
1018
+ content.push({ contentType: getRequestContentType({ document, operation }) || 'application/json', ...parseEntrySchema(ctx.contentType) })
1019
+ }
904
1020
 
905
- return ast.createResponse({
1021
+ return ast.factory.createResponse({
906
1022
  statusCode: statusCode as ast.StatusCode,
907
1023
  description,
908
- schema,
909
- mediaType,
910
- keysToOmit: collectPropertyKeysByFlag(responseSchema, 'writeOnly'),
1024
+ content,
911
1025
  })
912
1026
  })
913
1027
 
914
- const urlPath = new URLPath(operation.path)
1028
+ const pathItem = document.paths?.[operation.path]
1029
+ const pathItemDoc = pathItem && !dialect.isReference(pathItem) ? (pathItem as { summary?: unknown; description?: unknown }) : undefined
1030
+ const pickDoc = (key: 'summary' | 'description'): string | undefined => {
1031
+ const own = operation.schema[key]
1032
+ if (typeof own === 'string') return own
1033
+ const fallback = pathItemDoc?.[key]
1034
+ return typeof fallback === 'string' ? fallback : undefined
1035
+ }
915
1036
 
916
- return ast.createOperation({
917
- operationId: operation.getOperationId(),
1037
+ return ast.factory.createOperation({
1038
+ operationId,
1039
+ protocol: 'http',
918
1040
  method: operation.method.toUpperCase() as ast.HttpMethod,
919
- path: urlPath.path,
920
- tags: operation.getTags().map((tag) => tag.name),
921
- summary: operation.getSummary() || undefined,
922
- description: operation.getDescription() || undefined,
923
- deprecated: operation.isDeprecated() || undefined,
1041
+ path: operation.path,
1042
+ tags: Array.isArray(operation.schema.tags) ? operation.schema.tags.map(String) : [],
1043
+ summary: pickDoc('summary') || undefined,
1044
+ description: pickDoc('description') || undefined,
1045
+ deprecated: operation.schema.deprecated || undefined,
924
1046
  parameters,
925
1047
  requestBody,
926
1048
  responses,
@@ -933,10 +1055,11 @@ function createSchemaParser(ctx: OasParserContext) {
933
1055
  /**
934
1056
  * Parses a single OpenAPI `SchemaObject` into a `SchemaNode`.
935
1057
  *
936
- * Use this for targeted schema parsing when you don't need the full spec.
937
- * For complete spec parsing, use `parseOas()` instead which handles operations and all schemas together.
1058
+ * Reach for this when you only need one schema, not the whole spec. To parse a full spec
1059
+ * with its operations and schemas, call `parseOas()`.
938
1060
  *
939
- * @note Circular schema references are tracked via internal state and resolve appropriately.
1061
+ * @note Internal state tracks `$ref` paths under resolution, so circular schemas stop
1062
+ * recursing instead of looping.
940
1063
  *
941
1064
  * @example
942
1065
  * ```ts
@@ -957,8 +1080,8 @@ export function parseSchema(
957
1080
  * Parses an OpenAPI specification into Kubb's universal `InputNode` AST.
958
1081
  *
959
1082
  * This is the main entry point for `@kubb/adapter-oas`. It converts OpenAPI/Swagger specs into a spec-agnostic tree
960
- * that downstream plugins (`plugin-ts`, `plugin-zod`, etc.) consume for code generation. No code is generated here
961
- * the tree is a pure data structure representing all schemas and operations.
1083
+ * that downstream plugins (`plugin-ts`, `plugin-zod`, etc.) consume for code generation. No code is generated here.
1084
+ * The tree is a pure data structure of all schemas and operations.
962
1085
  *
963
1086
  * Returns the AST root and a `nameMapping` for resolving schema references.
964
1087
  *
@@ -987,16 +1110,11 @@ export function parseOas(
987
1110
 
988
1111
  const schemas: Array<ast.SchemaNode> = Object.entries(schemaObjects).map(([name, schema]) => _parseSchema({ schema, name }, mergedOptions))
989
1112
 
990
- const baseOas = new BaseOas(document)
991
- const paths = baseOas.getPaths()
992
-
993
- const operations: Array<ast.OperationNode> = Object.entries(paths).flatMap(([_path, methods]) =>
994
- Object.entries(methods)
995
- .map(([, operation]) => (operation ? _parseOperation(mergedOptions, operation) : null))
996
- .filter((op): op is ast.OperationNode => op !== null),
997
- )
1113
+ const operations: Array<ast.OperationNode> = getOperations(document)
1114
+ .map((operation) => _parseOperation(mergedOptions, operation))
1115
+ .filter((op): op is ast.OperationNode => op !== null)
998
1116
 
999
- const root = ast.createInput({ schemas, operations })
1117
+ const root = ast.factory.createInput({ schemas, operations })
1000
1118
 
1001
1119
  return { root, nameMapping }
1002
1120
  }