@kubb/adapter-oas 5.0.0-alpha.15 → 5.0.0-alpha.17

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,120 +1,47 @@
1
- import { pascalCase, URLPath } from '@internals/utils'
2
- import { createOperation, createParameter, createProperty, createResponse, createRoot, createSchema, narrowSchema, schemaTypes, transform } from '@kubb/ast'
1
+ import { URLPath } from '@internals/utils'
2
+ import {
3
+ childName,
4
+ createDiscriminantNode,
5
+ createOperation,
6
+ createParameter,
7
+ createProperty,
8
+ createResponse,
9
+ createRoot,
10
+ createSchema,
11
+ enumPropName,
12
+ extractRefName,
13
+ findDiscriminator,
14
+ type InferSchemaNode,
15
+ mediaTypes,
16
+ mergeAdjacentObjects,
17
+ narrowSchema,
18
+ type ParserOptions,
19
+ resolveNames,
20
+ schemaTypes,
21
+ setDiscriminatorEnum,
22
+ setEnumName,
23
+ simplifyUnion,
24
+ } from '@kubb/ast'
3
25
  import type {
4
- ArraySchemaNode,
5
- DateSchemaNode,
6
- DatetimeSchemaNode,
7
- EnumSchemaNode,
26
+ DistributiveOmit,
8
27
  HttpMethod,
9
- IntersectionSchemaNode,
10
28
  MediaType,
11
- NumberSchemaNode,
12
- ObjectSchemaNode,
13
29
  OperationNode,
14
30
  ParameterLocation,
15
31
  ParameterNode,
16
32
  PrimitiveSchemaType,
17
33
  PropertyNode,
18
- RefSchemaNode,
19
34
  ResponseNode,
20
35
  RootNode,
21
- ScalarSchemaNode,
22
36
  ScalarSchemaType,
23
37
  SchemaNode,
24
38
  SchemaType,
25
39
  StatusCode,
26
- StringSchemaNode,
27
- TimeSchemaNode,
28
- UnionSchemaNode,
29
40
  } from '@kubb/ast/types'
30
- import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, formatMap, knownMediaTypes } from './constants.ts'
41
+ import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, formatMap } from './constants.ts'
31
42
  import type { Oas } from './oas/Oas.ts'
32
43
  import type { contentType, Operation, ReferenceObject, SchemaObject } from './oas/types.ts'
33
44
  import { flattenSchema, isDiscriminator, isNullable, isReference } from './oas/utils.ts'
34
- import type { ParserOptions } from './types.ts'
35
- import { applyDiscriminatorEnum, extractRefName, mergeAdjacentAnonymousObjects, simplifyUnionMembers } from './utils.ts'
36
-
37
- /**
38
- * Distributive `Omit` — correctly distributes over union types so that
39
- * `Omit<A | B, 'kind'>` produces `Omit<A, 'kind'> | Omit<B, 'kind'>`
40
- * rather than `Omit<A | B, 'kind'>`.
41
- */
42
- type DistributiveOmit<TValue, TKey extends PropertyKey> = TValue extends unknown ? Omit<TValue, TKey> : never
43
-
44
- /**
45
- * Maps each `dateType` option value to the AST node produced by `format: 'date-time'`.
46
- */
47
- type DateTimeNodeByDateType = {
48
- date: DateSchemaNode
49
- string: DatetimeSchemaNode
50
- stringOffset: DatetimeSchemaNode
51
- stringLocal: DatetimeSchemaNode
52
- false: StringSchemaNode
53
- }
54
-
55
- /**
56
- * Resolves the AST node produced by `format: 'date-time'` based on the `dateType` option.
57
- */
58
- type ResolveDateTimeNode<TDateType extends ParserOptions['dateType']> = DateTimeNodeByDateType[TDateType extends keyof DateTimeNodeByDateType
59
- ? TDateType
60
- : 'string']
61
-
62
- /**
63
- * Single source of truth: ordered list of `[shape, SchemaNode]` pairs.
64
- * `InferSchemaNode` walks this tuple in order and returns the node type of the first matching entry.
65
- * Parameterized over `TDateType` so `format: 'date-time'` resolves to the correct node based on the option.
66
- */
67
- type SchemaNodeMap<TDateType extends ParserOptions['dateType'] = 'string'> = [
68
- [{ $ref: string }, RefSchemaNode],
69
- // allOf with sibling `properties` always produces an intersection (shared props are appended as a member).
70
- [{ allOf: ReadonlyArray<unknown>; properties: object }, IntersectionSchemaNode],
71
- // allOf with 2+ members always produces an intersection.
72
- [{ allOf: readonly [unknown, unknown, ...unknown[]] }, IntersectionSchemaNode],
73
- // Single-member allOf without sibling `properties` flattens to the member type.
74
- [{ allOf: ReadonlyArray<unknown> }, SchemaNode],
75
- [{ oneOf: ReadonlyArray<unknown> }, UnionSchemaNode],
76
- [{ anyOf: ReadonlyArray<unknown> }, UnionSchemaNode],
77
- [{ const: null }, ScalarSchemaNode],
78
- [{ const: string | number | boolean }, EnumSchemaNode],
79
- // OAS 3.1 multi-type array: `{ type: ['string', 'integer'] }` → union node.
80
- [{ type: ReadonlyArray<string> }, UnionSchemaNode],
81
- // `{ type: 'array', enum }` is normalized at runtime: enum moves into items → array node.
82
- [{ type: 'array'; enum: ReadonlyArray<unknown> }, ArraySchemaNode],
83
- [{ enum: ReadonlyArray<unknown> }, EnumSchemaNode],
84
- [{ type: 'object' }, ObjectSchemaNode],
85
- [{ additionalProperties: boolean | {} }, ObjectSchemaNode],
86
- [{ type: 'array' }, ArraySchemaNode],
87
- [{ items: object }, ArraySchemaNode],
88
- [{ prefixItems: ReadonlyArray<unknown> }, ArraySchemaNode],
89
- // Format entries with explicit type — placed before generic type entries so format wins.
90
- [{ type: string; format: 'date-time' }, ResolveDateTimeNode<TDateType>],
91
- [{ type: string; format: 'date' }, DateSchemaNode],
92
- [{ type: string; format: 'time' }, TimeSchemaNode],
93
- [{ format: 'date-time' }, ResolveDateTimeNode<TDateType>],
94
- [{ format: 'date' }, DateSchemaNode],
95
- [{ format: 'time' }, TimeSchemaNode],
96
- [{ type: 'string' }, StringSchemaNode],
97
- [{ type: 'number' }, NumberSchemaNode],
98
- [{ type: 'integer' }, NumberSchemaNode],
99
- [{ type: 'bigint' }, NumberSchemaNode],
100
- [{ type: string }, ScalarSchemaNode],
101
- // Inferred scalar types from constraints when no explicit type is present.
102
- [{ minLength: number }, StringSchemaNode],
103
- [{ maxLength: number }, StringSchemaNode],
104
- [{ pattern: string }, StringSchemaNode],
105
- [{ minimum: number }, NumberSchemaNode],
106
- [{ maximum: number }, NumberSchemaNode],
107
- ]
108
-
109
- export type InferSchemaNode<
110
- TSchema extends SchemaObject,
111
- TDateType extends ParserOptions['dateType'] = 'string',
112
- TEntries extends ReadonlyArray<[object, SchemaNode]> = SchemaNodeMap<TDateType>,
113
- > = TEntries extends [infer TEntry extends [object, SchemaNode], ...infer TRest extends ReadonlyArray<[object, SchemaNode]>]
114
- ? TSchema extends TEntry[0]
115
- ? TEntry[1]
116
- : InferSchemaNode<TSchema, TDateType, TRest>
117
- : SchemaNode
118
45
 
119
46
  /**
120
47
  * Construction-time options for `createOasParser`.
@@ -148,7 +75,7 @@ function getPrimitiveType(type: string | undefined): PrimitiveSchemaType {
148
75
  * Returns `undefined` for content types not present in `KNOWN_MEDIA_TYPES`.
149
76
  */
150
77
  function toMediaType(contentType: string): MediaType | undefined {
151
- return knownMediaTypes.has(contentType as MediaType) ? (contentType as MediaType) : undefined
78
+ return Object.values(mediaTypes).includes(contentType as MediaType) ? (contentType as MediaType) : undefined
152
79
  }
153
80
 
154
81
  /**
@@ -164,8 +91,8 @@ type SchemaContext = {
164
91
  * Normalized single type string (first element when OAS 3.1 multi-type array).
165
92
  */
166
93
  type: string | undefined
167
- options: Partial<ParserOptions> | undefined
168
- mergedOptions: ParserOptions
94
+ rawOptions: Partial<ParserOptions> | undefined
95
+ options: ParserOptions
169
96
  }
170
97
 
171
98
  /**
@@ -195,7 +122,7 @@ export type OasParser = {
195
122
  * Map from original `$ref` paths to their collision-resolved schema names.
196
123
  * e.g. `'#/components/schemas/Order'` → `'OrderSchema'`
197
124
  *
198
- * Pass this to the standalone `getImports()` to resolve imports without holding
125
+ * Pass this to the standalone `collectImports()` to resolve imports without holding
199
126
  * a reference to the full parser or OAS instance.
200
127
  */
201
128
  nameMapping: Map<string, string>
@@ -225,14 +152,25 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
225
152
  // e.g., { '#/components/schemas/Order': 'OrderSchema', '#/components/responses/Product': 'ProductResponse' }
226
153
  const { schemas: schemaObjects, nameMapping } = oas.getSchemas({ contentType })
227
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
+
228
161
  /**
229
162
  * Maps an `'any' | 'unknown' | 'void'` option string to the corresponding `SchemaType` constant.
230
163
  * Used for both `unknownType` (unannotated schemas) and `emptySchemaType` (empty `{}` schemas).
231
164
  */
232
165
  function resolveTypeOption(value: 'any' | 'unknown' | 'void'): ScalarSchemaType {
233
- if (value === 'any') return schemaTypes.any
234
- if (value === 'void') return schemaTypes.void
235
- return schemaTypes.unknown
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
236
174
  }
237
175
 
238
176
  /**
@@ -328,7 +266,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
328
266
  * Circular references through discriminator parents are detected and skipped to prevent
329
267
  * infinite recursion during code generation.
330
268
  */
331
- function convertAllOf({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode {
269
+ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
332
270
  if (
333
271
  schema.allOf!.length === 1 &&
334
272
  !schema.properties &&
@@ -336,7 +274,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
336
274
  schema.additionalProperties === undefined
337
275
  ) {
338
276
  const [memberSchema] = schema.allOf as Array<SchemaObject | ReferenceObject>
339
- const memberNode = convertSchema({ schema: memberSchema! as SchemaObject }, options)
277
+ const memberNode = convertSchema({ schema: memberSchema! as SchemaObject }, rawOptions)
340
278
  const { kind: _kind, ...memberNodeProps } = memberNode
341
279
  const mergedNullable = nullable || memberNode.nullable || undefined
342
280
  const mergedDefault = schema.default === null && mergedNullable ? undefined : (schema.default ?? memberNode.default)
@@ -371,7 +309,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
371
309
  const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef)
372
310
  const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef)
373
311
  if (inOneOf || inMapping) {
374
- const discriminatorValue = Object.entries(deref.discriminator.mapping ?? {}).find(([, v]) => v === childRef)?.[0]
312
+ const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef)
375
313
  if (discriminatorValue) {
376
314
  filteredDiscriminantValues.push({ propertyName: deref.discriminator.propertyName, value: discriminatorValue })
377
315
  }
@@ -379,7 +317,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
379
317
  }
380
318
  return true
381
319
  })
382
- .map((s) => convertSchema({ schema: s as SchemaObject }, options))
320
+ .map((s) => convertSchema({ schema: s as SchemaObject }, rawOptions))
383
321
 
384
322
  // Track where allOf-derived members end so each portion can be merged independently.
385
323
  const syntheticStart = allOfMembers.length
@@ -400,7 +338,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
400
338
  for (const key of missingRequired) {
401
339
  for (const resolved of resolvedMembers) {
402
340
  if (resolved.properties?.[key]) {
403
- allOfMembers.push(convertSchema({ schema: { properties: { [key]: resolved.properties[key] }, required: [key] } as SchemaObject }, options))
341
+ allOfMembers.push(convertSchema({ schema: { properties: { [key]: resolved.properties[key] }, required: [key] } as SchemaObject }, rawOptions))
404
342
  break
405
343
  }
406
344
  }
@@ -410,35 +348,19 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
410
348
 
411
349
  if (schema.properties) {
412
350
  const { allOf: _allOf, ...schemaWithoutAllOf } = schema
413
- allOfMembers.push(convertSchema({ schema: schemaWithoutAllOf }, options))
351
+ allOfMembers.push(convertSchema({ schema: schemaWithoutAllOf }, rawOptions))
414
352
  }
415
353
 
416
354
  // Inject a synthetic single-property object for each discriminant value collected from
417
355
  // filtered discriminator parents so that child schemas carry the narrowed literal type.
418
356
  for (const { propertyName, value } of filteredDiscriminantValues) {
419
- allOfMembers.push(
420
- createSchema({
421
- type: 'object',
422
- primitive: 'object',
423
- properties: [
424
- createProperty({
425
- name: propertyName,
426
- schema: createSchema({
427
- type: 'enum',
428
- primitive: 'string',
429
- enumValues: [value],
430
- }),
431
- required: true,
432
- }),
433
- ],
434
- }),
435
- )
357
+ allOfMembers.push(createDiscriminantNode({ propertyName, value }))
436
358
  }
437
359
 
438
- // Merge consecutive anonymous object members within the synthetic portion — see `mergeAdjacentAnonymousObjects`.
360
+ // Merge consecutive anonymous object members within the synthetic portion — see `mergeAdjacentObjects`.
439
361
  return createSchema({
440
362
  type: 'intersection',
441
- members: [...mergeAdjacentAnonymousObjects(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentAnonymousObjects(allOfMembers.slice(syntheticStart))],
363
+ members: [...mergeAdjacentObjects(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentObjects(allOfMembers.slice(syntheticStart))],
442
364
  ...renderSchemaBase(schema, name, nullable, defaultValue),
443
365
  })
444
366
  }
@@ -451,90 +373,89 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
451
373
  * individually intersected with the shared properties node to match the OAS pattern of
452
374
  * adding common fields next to a discriminated union.
453
375
  */
454
- function convertUnion({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode {
376
+ function convertUnion({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
377
+ function pickDiscriminatorPropertyNode(node: SchemaNode, propertyName: string): SchemaNode | undefined {
378
+ const objectNode = narrowSchema(node, 'object')
379
+ const discriminatorProperty = objectNode?.properties?.find((property) => property.name === propertyName)
380
+
381
+ if (!discriminatorProperty) {
382
+ return undefined
383
+ }
384
+
385
+ return createSchema({
386
+ type: 'object',
387
+ primitive: 'object',
388
+ properties: [discriminatorProperty],
389
+ })
390
+ }
391
+
455
392
  const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
456
393
  const unionBase = {
457
394
  ...renderSchemaBase(schema, name, nullable, defaultValue),
458
395
  discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
459
396
  }
397
+ const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
398
+ const sharedPropertiesNode = schema.properties
399
+ ? (() => {
400
+ const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
401
+ // Strip discriminator so convertObject won't re-apply the full mapping enum.
402
+ const memberBaseSchema: SchemaObject = discriminator
403
+ ? (Object.fromEntries(Object.entries(schemaWithoutUnion).filter(([key]) => key !== 'discriminator')) as SchemaObject)
404
+ : 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)
408
+ })()
409
+ : undefined
460
410
 
461
- if (schema.properties) {
462
- const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
463
- const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
464
-
465
- // Strip discriminator so convertObject won't re-apply the full mapping enum.
466
- const memberBaseSchema: SchemaObject = discriminator
467
- ? (Object.fromEntries(Object.entries(schemaWithoutUnion).filter(([key]) => key !== 'discriminator')) as SchemaObject)
468
- : schemaWithoutUnion
469
-
470
- // Convert shared properties once to avoid duplicate enum naming
471
- // (e.g. StatusEnum appearing twice and getting a numeric suffix).
472
- const sharedPropertiesNode = convertSchema({ schema: memberBaseSchema, name }, options)
473
-
474
- return createSchema({
475
- type: 'union',
476
- ...unionBase,
477
- members: unionMembers.map((s) => {
478
- const ref = isReference(s) ? s.$ref : undefined
479
- const discriminatorValue = discriminator?.mapping && ref ? Object.entries(discriminator.mapping).find(([, v]) => v === ref)?.[0] : undefined
411
+ if (sharedPropertiesNode || discriminator?.mapping) {
412
+ const members = unionMembers.map((s) => {
413
+ const ref = isReference(s) ? s.$ref : undefined
414
+ const discriminatorValue = findDiscriminator(discriminator?.mapping, ref)
415
+ const memberNode = convertSchema({ schema: s as SchemaObject }, rawOptions)
480
416
 
481
- let propertiesNode = sharedPropertiesNode
417
+ if (!discriminatorValue || !discriminator) {
418
+ return memberNode
419
+ }
482
420
 
483
- if (discriminatorValue && discriminator) {
484
- propertiesNode = applyDiscriminatorEnum({ node: propertiesNode, propertyName: discriminator.propertyName, values: [discriminatorValue] })
485
- }
421
+ const narrowedDiscriminatorNode = sharedPropertiesNode
422
+ ? pickDiscriminatorPropertyNode(
423
+ setDiscriminatorEnum({
424
+ node: sharedPropertiesNode,
425
+ propertyName: discriminator.propertyName,
426
+ values: [discriminatorValue],
427
+ }),
428
+ discriminator.propertyName,
429
+ )
430
+ : undefined
486
431
 
487
- return createSchema({
488
- type: 'intersection',
489
- members: [convertSchema({ schema: s as SchemaObject }, options), propertiesNode],
490
- })
491
- }),
432
+ return createSchema({
433
+ type: 'intersection',
434
+ members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({ propertyName: discriminator.propertyName, value: discriminatorValue })],
435
+ })
492
436
  })
493
- }
494
437
 
495
- // When a discriminator with mapping is present but there are no sibling properties,
496
- // embed the narrowed discriminant value into each member to produce precise literal
497
- // intersection types (e.g. `Cat & { type: 'cat' }`) instead of plain `Cat | Dog`.
498
- const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
499
- if (discriminator?.mapping) {
500
- return createSchema({
438
+ const unionNode = createSchema({
501
439
  type: 'union',
502
440
  ...unionBase,
503
- members: unionMembers.map((s) => {
504
- const ref = isReference(s) ? s.$ref : undefined
505
- const discriminatorValue = ref ? Object.entries(discriminator.mapping!).find(([, v]) => v === ref)?.[0] : undefined
506
- const memberNode = convertSchema({ schema: s as SchemaObject }, options)
507
-
508
- if (!discriminatorValue) return memberNode
509
-
510
- const discriminantNode = createSchema({
511
- type: 'object',
512
- primitive: 'object',
513
- properties: [
514
- createProperty({
515
- name: discriminator.propertyName,
516
- schema: createSchema({
517
- type: 'enum',
518
- primitive: 'string',
519
- enumValues: [discriminatorValue],
520
- }),
521
- required: true,
522
- }),
523
- ],
524
- })
441
+ members,
442
+ })
525
443
 
526
- return createSchema({
527
- type: 'intersection',
528
- members: [memberNode, discriminantNode],
529
- })
530
- }),
444
+ if (!sharedPropertiesNode) {
445
+ return unionNode
446
+ }
447
+
448
+ return createSchema({
449
+ type: 'intersection',
450
+ ...renderSchemaBase(schema, name, nullable, defaultValue),
451
+ members: [unionNode, sharedPropertiesNode],
531
452
  })
532
453
  }
533
454
 
534
455
  return createSchema({
535
456
  type: 'union',
536
457
  ...unionBase,
537
- members: simplifyUnionMembers(unionMembers.map((s) => convertSchema({ schema: s as SchemaObject }, options))),
458
+ members: simplifyUnion(unionMembers.map((s) => convertSchema({ schema: s as SchemaObject }, rawOptions))),
538
459
  })
539
460
  }
540
461
 
@@ -573,13 +494,13 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
573
494
  * Returns `undefined` when the format should fall through to string handling
574
495
  * (i.e. `format: 'date-time'` with `dateType: false`).
575
496
  */
576
- function convertFormat({ schema, name, nullable, defaultValue, mergedOptions }: SchemaContext): SchemaNode | undefined {
497
+ function convertFormat({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode | undefined {
577
498
  const base = renderSchemaBase(schema, name, nullable, defaultValue)
578
499
 
579
500
  // int64 is option-dependent so it can't live in the static formatMap.
580
501
  if (schema.format === 'int64') {
581
502
  return createSchema({
582
- type: mergedOptions.integerType === 'bigint' ? 'bigint' : 'integer',
503
+ type: options.integerType === 'bigint' ? 'bigint' : 'integer',
583
504
  primitive: 'integer',
584
505
  ...base,
585
506
  min: schema.minimum,
@@ -591,7 +512,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
591
512
 
592
513
  // date-time / date / time are option-dependent and can't live in the static formatMap.
593
514
  if (schema.format === 'date-time' || schema.format === 'date' || schema.format === 'time') {
594
- const dateType = getDateType(mergedOptions, schema.format)
515
+ const dateType = getDateType(options, schema.format)
595
516
  if (!dateType) return undefined // dateType: false → fall through to string
596
517
 
597
518
  if (dateType.type === 'datetime') {
@@ -625,13 +546,10 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
625
546
  * - Numeric and boolean enums require a const-map representation because most generators cannot
626
547
  * use string-enum syntax for non-string values.
627
548
  */
628
- function convertEnum({ schema, name, nullable, type, options }: SchemaContext): SchemaNode {
549
+ function convertEnum({ schema, name, nullable, type, rawOptions }: SchemaContext): SchemaNode {
629
550
  // Malformed schema: `{ type: 'array', enum: [...] }` — normalize by moving the enum into items.
630
551
  if (type === 'array') {
631
- const isItemsObject = typeof schema.items === 'object' && !Array.isArray(schema.items)
632
- const normalizedItems: SchemaObject = { ...(isItemsObject ? (schema.items as SchemaObject) : {}), enum: schema.enum }
633
- const { enum: _enum, ...schemaWithoutEnum } = schema
634
- return convertSchema({ schema: { ...schemaWithoutEnum, items: normalizedItems } as SchemaObject, name }, options)
552
+ return convertSchema({ schema: normalizeArrayEnum(schema), name }, rawOptions)
635
553
  }
636
554
 
637
555
  // `null` in enum values is the OAS 3.0 convention for a nullable enum.
@@ -657,49 +575,22 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
657
575
 
658
576
  // x-enumNames / x-enum-varnames: named variants with explicit labels take priority.
659
577
  const extensionKey = enumExtensionKeys.find((key) => key in schema)
660
- if (extensionKey) {
661
- const rawNames = (schema as Record<string, unknown>)[extensionKey] as Array<string | number>
662
- const uniqueNames = [...new Set(rawNames)]
663
- const enumType =
664
- getPrimitiveType(type) === 'number' || getPrimitiveType(type) === 'integer'
665
- ? ('number' as const)
666
- : getPrimitiveType(type) === 'boolean'
667
- ? ('boolean' as const)
668
- : ('string' as const)
578
+ if (extensionKey || enumPrimitive === 'number' || enumPrimitive === 'integer' || enumPrimitive === 'boolean') {
579
+ const enumPrimitiveType = (enumPrimitive === 'number' || enumPrimitive === 'integer' ? 'number' : enumPrimitive === 'boolean' ? 'boolean' : 'string') as
580
+ | 'number'
581
+ | 'boolean'
582
+ | 'string'
583
+ const sourceValues = extensionKey
584
+ ? [...new Set((schema as Record<string, unknown>)[extensionKey] as Array<string | number>)]
585
+ : [...new Set(filteredValues)]
669
586
 
670
587
  return createSchema({
671
588
  ...enumBase,
672
- enumType,
673
- namedEnumValues: uniqueNames.map((label, index) => ({
589
+ primitive: enumPrimitiveType,
590
+ namedEnumValues: sourceValues.map((label, index) => ({
674
591
  name: String(label),
675
- value: filteredValues[index] ?? label,
676
- format: enumType,
677
- })),
678
- })
679
- }
680
-
681
- // Number / integer enum — must use a const map since most generators can't use string-enum for numbers.
682
- if (type === 'number' || type === 'integer') {
683
- return createSchema({
684
- ...enumBase,
685
- enumType: 'number' as const,
686
- namedEnumValues: [...new Set(filteredValues)].map((value) => ({
687
- name: String(value),
688
- value: value as number,
689
- format: 'number' as const,
690
- })),
691
- })
692
- }
693
-
694
- // Boolean enum — same const-map approach as numeric.
695
- if (type === 'boolean') {
696
- return createSchema({
697
- ...enumBase,
698
- enumType: 'boolean' as const,
699
- namedEnumValues: [...new Set(filteredValues)].map((value) => ({
700
- name: String(value),
701
- value: value as boolean,
702
- format: 'boolean' as const,
592
+ value: extensionKey ? (filteredValues[index] ?? label) : label,
593
+ primitive: enumPrimitiveType,
703
594
  })),
704
595
  })
705
596
  }
@@ -723,61 +614,21 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
723
614
  * - not required + not nullable → `optional: true`
724
615
  * - not required + nullable → `nullish: true`
725
616
  */
726
- /**
727
- * Builds the propagation name for a child property during recursive schema conversion.
728
- *
729
- * The parent name is prepended so the full path is encoded
730
- * (e.g. `OrderParams` when parent is `Order`).
731
- */
732
- function resolveChildName(parentName: string | undefined, propName: string): string | undefined {
733
- return parentName ? pascalCase([parentName, propName].join(' ')) : undefined
734
- }
735
-
736
- /**
737
- * Derives the final name for an enum property schema node.
738
- *
739
- * The resulting name always includes the enum suffix and full parent path context
740
- * (e.g. `OrderParamsStatusEnum`).
741
- */
742
- function resolveEnumPropName(parentName: string | undefined, propName: string, enumSuffix: string): string {
743
- return pascalCase([parentName, propName, enumSuffix].filter(Boolean).join(' '))
744
- }
745
-
746
- /**
747
- * Given a freshly-converted property schema, returns the node with a correct
748
- * `name` attached — or stripped — depending on whether the node is a named
749
- * enum, a boolean const-enum (always inlined), or a regular schema.
750
- */
751
- function applyEnumName(propNode: SchemaNode, parentName: string | undefined, propName: string, enumSuffix: string): SchemaNode {
752
- const enumNode = narrowSchema(propNode, 'enum')
753
-
754
- // Boolean-primitive enum nodes (e.g. `const: false`) are always inlined as
755
- // literal types and must not receive a named identifier.
756
- if (enumNode?.primitive === 'boolean') {
757
- return { ...propNode, name: undefined }
758
- }
759
-
760
- if (enumNode) {
761
- return { ...propNode, name: resolveEnumPropName(parentName, propName, enumSuffix) }
762
- }
763
617
 
764
- return propNode
765
- }
766
-
767
- function convertObject({ schema, name, nullable, defaultValue, options, mergedOptions }: SchemaContext): SchemaNode {
618
+ function convertObject({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): SchemaNode {
768
619
  const properties: Array<PropertyNode> = schema.properties
769
620
  ? Object.entries(schema.properties).map(([propName, propSchema]) => {
770
621
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required
771
622
  const resolvedPropSchema = propSchema as SchemaObject
772
623
  const propNullable = isNullable(resolvedPropSchema)
773
624
 
774
- const childName = resolveChildName(name, propName)
775
- const propNode = convertSchema({ schema: resolvedPropSchema, name: childName }, options)
776
- let schemaNode = applyEnumName(propNode, name, propName, mergedOptions.enumSuffix)
625
+ const resolvedChildName = childName(name, propName)
626
+ const propNode = convertSchema({ schema: resolvedPropSchema, name: resolvedChildName }, rawOptions)
627
+ let schemaNode = setEnumName(propNode, name, propName, options.enumSuffix)
777
628
 
778
629
  const tupleNode = narrowSchema(schemaNode, 'tuple')
779
630
  if (tupleNode?.items) {
780
- const namedItems = tupleNode.items.map((item) => applyEnumName(item, name, propName, mergedOptions.enumSuffix))
631
+ const namedItems = tupleNode.items.map((item) => setEnumName(item, name, propName, options.enumSuffix))
781
632
  if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
782
633
  schemaNode = { ...tupleNode, items: namedItems }
783
634
  }
@@ -799,11 +650,11 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
799
650
  if (additionalProperties === true) {
800
651
  additionalPropertiesNode = true
801
652
  } else if (additionalProperties && Object.keys(additionalProperties).length > 0) {
802
- additionalPropertiesNode = convertSchema({ schema: additionalProperties as SchemaObject }, options)
653
+ additionalPropertiesNode = convertSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
803
654
  } else if (additionalProperties === false) {
804
655
  additionalPropertiesNode = undefined
805
656
  } else if (additionalProperties) {
806
- additionalPropertiesNode = createSchema({ type: resolveTypeOption(mergedOptions.unknownType) })
657
+ additionalPropertiesNode = createSchema({ type: resolveTypeOption(options.unknownType) })
807
658
  }
808
659
 
809
660
  const rawPatternProperties = 'patternProperties' in schema ? schema.patternProperties : undefined
@@ -813,8 +664,8 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
813
664
  Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [
814
665
  pattern,
815
666
  patternSchema === true || (typeof patternSchema === 'object' && Object.keys(patternSchema).length === 0)
816
- ? createSchema({ type: resolveTypeOption(mergedOptions.unknownType) })
817
- : convertSchema({ schema: patternSchema as SchemaObject }, options),
667
+ ? createSchema({ type: resolveTypeOption(options.unknownType) })
668
+ : convertSchema({ schema: patternSchema as SchemaObject }, rawOptions),
818
669
  ]),
819
670
  )
820
671
  : undefined
@@ -833,8 +684,8 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
833
684
  if (isDiscriminator(schema) && schema.discriminator.mapping) {
834
685
  const discPropName = schema.discriminator.propertyName
835
686
  const values = Object.keys(schema.discriminator.mapping)
836
- const enumName = name ? resolveEnumPropName(name, discPropName, mergedOptions.enumSuffix) : undefined
837
- return applyDiscriminatorEnum({ node: objectNode, propertyName: discPropName, values, enumName })
687
+ const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : undefined
688
+ return setDiscriminatorEnum({ node: objectNode, propertyName: discPropName, values, enumName })
838
689
  }
839
690
 
840
691
  return objectNode
@@ -848,9 +699,9 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
848
699
  * a rest element of type `any` is emitted to match JSON Schema semantics (absent `items`
849
700
  * means additional items are allowed).
850
701
  */
851
- function convertTuple({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode {
852
- const tupleItems = (schema.prefixItems ?? []).map((item) => convertSchema({ schema: item as SchemaObject }, options))
853
- const rest = schema.items ? convertSchema({ schema: schema.items as SchemaObject }, options) : createSchema({ type: 'any' })
702
+ 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' })
854
705
 
855
706
  return createSchema({
856
707
  type: 'tuple',
@@ -869,12 +720,12 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
869
720
  * When the items schema is an inline enum, a name derived from the parent array's name and
870
721
  * `enumSuffix` is forwarded so generators can emit a named enum declaration.
871
722
  */
872
- function convertArray({ schema, name, nullable, defaultValue, options, mergedOptions }: SchemaContext): SchemaNode {
723
+ function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): SchemaNode {
873
724
  const rawItems = schema.items as SchemaObject | undefined
874
725
  // When the items schema contains an inline enum, derive a named identifier
875
726
  // so generators can emit a standalone enum declaration.
876
- const itemName = rawItems?.enum?.length && name ? resolveEnumPropName(undefined, name, mergedOptions.enumSuffix) : undefined
877
- const items = rawItems ? [convertSchema({ schema: rawItems, name: itemName }, options)] : []
727
+ const itemName = rawItems?.enum?.length && name ? enumPropName(undefined, name, options.enumSuffix) : undefined
728
+ const items = rawItems ? [convertSchema({ schema: rawItems, name: itemName }, rawOptions)] : []
878
729
 
879
730
  return createSchema({
880
731
  type: 'array',
@@ -958,13 +809,13 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
958
809
  * 10. Object / array / tuple / scalar by `type`
959
810
  * 11. Empty schema fallback (`emptySchemaType` option)
960
811
  */
961
- function convertSchema({ schema, name }: { schema: SchemaObject; name?: string }, options?: Partial<ParserOptions>): SchemaNode {
962
- const mergedOptions: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...options }
812
+ function convertSchema({ schema, name }: { schema: SchemaObject; name?: string }, rawOptions?: Partial<ParserOptions>): SchemaNode {
813
+ const options: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...rawOptions }
963
814
  // Flatten keyword-only allOf fragments (no $ref, no structural keys) into the parent
964
815
  // schema before parsing, so simple annotation patterns don't produce needless intersections.
965
816
  const flattenedSchema = flattenSchema(schema)
966
817
  if (flattenedSchema && flattenedSchema !== schema) {
967
- return convertSchema({ schema: flattenedSchema, name }, options)
818
+ return convertSchema({ schema: flattenedSchema, name }, rawOptions)
968
819
  }
969
820
 
970
821
  const nullable = isNullable(schema) || undefined
@@ -972,7 +823,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
972
823
  // Normalize OAS 3.1 multi-type array to a single type string for the dispatch below.
973
824
  const type = Array.isArray(schema.type) ? schema.type[0] : schema.type
974
825
 
975
- const ctx: SchemaContext = { schema, name, nullable, defaultValue, type, options, mergedOptions }
826
+ const ctx: SchemaContext = { schema, name, nullable, defaultValue, type, rawOptions, options }
976
827
 
977
828
  // $ref — pointer to another definition.
978
829
  // In OAS 3.0 siblings of $ref are technically ignored, but Kubb intentionally preserves them
@@ -1011,7 +862,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
1011
862
  if (nonNullTypes.length > 1) {
1012
863
  return createSchema({
1013
864
  type: 'union',
1014
- members: nonNullTypes.map((t) => convertSchema({ schema: { ...schema, type: t } as SchemaObject, name }, options)),
865
+ members: nonNullTypes.map((t) => convertSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
1015
866
  ...renderSchemaBase(schema, name, arrayNullable, defaultValue),
1016
867
  })
1017
868
  }
@@ -1039,7 +890,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
1039
890
  if (type === 'boolean') return convertBoolean(ctx)
1040
891
  if (type === 'null') return convertNull(ctx)
1041
892
 
1042
- const emptyType = resolveTypeOption(mergedOptions.emptySchemaType)
893
+ const emptyType = resolveTypeOption(options.emptySchemaType)
1043
894
  return createSchema({ type: emptyType as ScalarSchemaType, name, title: schema.title, description: schema.description })
1044
895
  }
1045
896
 
@@ -1163,36 +1014,11 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
1163
1014
  return createRoot({ schemas, operations })
1164
1015
  }
1165
1016
 
1166
- /**
1167
- * Walks a `SchemaNode` tree and resolves all `ref` node names through the provided callbacks.
1168
- *
1169
- * `resolveName` handles all schema types; `resolveEnumName` (when provided) takes precedence
1170
- * for `enum` nodes, enabling a separate naming strategy for enums (e.g. different suffix).
1171
- *
1172
- * Collision-resolved names (from `nameMapping`) are applied before user-supplied resolvers.
1173
- */
1174
- function resolveRefs(node: SchemaNode, resolveName: (ref: string) => string | undefined, resolveEnumName?: (name: string) => string | undefined): SchemaNode {
1175
- return transform(node, {
1176
- schema(schemaNode) {
1177
- const schemaRef = narrowSchema(schemaNode, schemaTypes.ref)
1178
-
1179
- if (schemaRef && (schemaRef.ref || schemaRef.name)) {
1180
- const rawRef = schemaRef.ref ?? schemaRef.name!
1181
- const resolved = resolveName(nameMapping.get(rawRef) ?? rawRef)
1182
- if (resolved) {
1183
- return { ...schemaNode, name: resolved }
1184
- }
1185
- }
1186
-
1187
- if (schemaNode.type === 'enum' && schemaNode.name) {
1188
- const resolved = (resolveEnumName ?? resolveName)(schemaNode.name)
1189
- if (resolved) {
1190
- return { ...schemaNode, name: resolved }
1191
- }
1192
- }
1193
- },
1194
- }) as SchemaNode
1195
- }
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 })
1196
1022
 
1197
1023
  return {
1198
1024
  parse,