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

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,44 @@
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
+ applyDiscriminatorEnum,
4
+ createOperation,
5
+ createParameter,
6
+ createProperty,
7
+ createResponse,
8
+ createRoot,
9
+ createSchema,
10
+ extractRefName,
11
+ mediaTypes,
12
+ mergeAdjacentAnonymousObjects,
13
+ narrowSchema,
14
+ schemaTypes,
15
+ simplifyUnionMembers,
16
+ } from '@kubb/ast'
3
17
  import type {
4
- ArraySchemaNode,
5
- DateSchemaNode,
6
- DatetimeSchemaNode,
7
- EnumSchemaNode,
18
+ DistributiveOmit,
8
19
  HttpMethod,
9
- IntersectionSchemaNode,
10
20
  MediaType,
11
- NumberSchemaNode,
12
- ObjectSchemaNode,
13
21
  OperationNode,
14
22
  ParameterLocation,
15
23
  ParameterNode,
16
24
  PrimitiveSchemaType,
17
25
  PropertyNode,
18
- RefSchemaNode,
19
26
  ResponseNode,
20
27
  RootNode,
21
- ScalarSchemaNode,
22
28
  ScalarSchemaType,
23
29
  SchemaNode,
24
30
  SchemaType,
25
31
  StatusCode,
26
- StringSchemaNode,
27
- TimeSchemaNode,
28
- UnionSchemaNode,
29
32
  } from '@kubb/ast/types'
30
- import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, formatMap, knownMediaTypes } from './constants.ts'
33
+ import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, formatMap } from './constants.ts'
34
+ import { createDiscriminantNode, resolveDiscriminatorValue } from './discriminator.ts'
35
+ import type { InferSchemaNode } from './infer.ts'
36
+ import { applyEnumName, resolveChildName, resolveEnumPropName } from './naming.ts'
31
37
  import type { Oas } from './oas/Oas.ts'
32
38
  import type { contentType, Operation, ReferenceObject, SchemaObject } from './oas/types.ts'
33
39
  import { flattenSchema, isDiscriminator, isNullable, isReference } from './oas/utils.ts'
40
+ import { resolveRefs as resolveRefsNode } from './refResolver.ts'
34
41
  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
42
 
119
43
  /**
120
44
  * Construction-time options for `createOasParser`.
@@ -148,7 +72,7 @@ function getPrimitiveType(type: string | undefined): PrimitiveSchemaType {
148
72
  * Returns `undefined` for content types not present in `KNOWN_MEDIA_TYPES`.
149
73
  */
150
74
  function toMediaType(contentType: string): MediaType | undefined {
151
- return knownMediaTypes.has(contentType as MediaType) ? (contentType as MediaType) : undefined
75
+ return Object.values(mediaTypes).includes(contentType as MediaType) ? (contentType as MediaType) : undefined
152
76
  }
153
77
 
154
78
  /**
@@ -164,8 +88,8 @@ type SchemaContext = {
164
88
  * Normalized single type string (first element when OAS 3.1 multi-type array).
165
89
  */
166
90
  type: string | undefined
167
- options: Partial<ParserOptions> | undefined
168
- mergedOptions: ParserOptions
91
+ rawOptions: Partial<ParserOptions> | undefined
92
+ options: ParserOptions
169
93
  }
170
94
 
171
95
  /**
@@ -225,14 +149,25 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
225
149
  // e.g., { '#/components/schemas/Order': 'OrderSchema', '#/components/responses/Product': 'ProductResponse' }
226
150
  const { schemas: schemaObjects, nameMapping } = oas.getSchemas({ contentType })
227
151
 
152
+ const TYPE_OPTION_MAP: Record<'any' | 'unknown' | 'void', ScalarSchemaType> = {
153
+ any: schemaTypes.any,
154
+ unknown: schemaTypes.unknown,
155
+ void: schemaTypes.void,
156
+ }
157
+
228
158
  /**
229
159
  * Maps an `'any' | 'unknown' | 'void'` option string to the corresponding `SchemaType` constant.
230
160
  * Used for both `unknownType` (unannotated schemas) and `emptySchemaType` (empty `{}` schemas).
231
161
  */
232
162
  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
163
+ return TYPE_OPTION_MAP[value]
164
+ }
165
+
166
+ function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
167
+ const isItemsObject = typeof schema.items === 'object' && !Array.isArray(schema.items)
168
+ const normalizedItems: SchemaObject = { ...(isItemsObject ? (schema.items as SchemaObject) : {}), enum: schema.enum }
169
+ const { enum: _enum, ...schemaWithoutEnum } = schema
170
+ return { ...schemaWithoutEnum, items: normalizedItems } as SchemaObject
236
171
  }
237
172
 
238
173
  /**
@@ -328,7 +263,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
328
263
  * Circular references through discriminator parents are detected and skipped to prevent
329
264
  * infinite recursion during code generation.
330
265
  */
331
- function convertAllOf({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode {
266
+ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
332
267
  if (
333
268
  schema.allOf!.length === 1 &&
334
269
  !schema.properties &&
@@ -336,7 +271,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
336
271
  schema.additionalProperties === undefined
337
272
  ) {
338
273
  const [memberSchema] = schema.allOf as Array<SchemaObject | ReferenceObject>
339
- const memberNode = convertSchema({ schema: memberSchema! as SchemaObject }, options)
274
+ const memberNode = convertSchema({ schema: memberSchema! as SchemaObject }, rawOptions)
340
275
  const { kind: _kind, ...memberNodeProps } = memberNode
341
276
  const mergedNullable = nullable || memberNode.nullable || undefined
342
277
  const mergedDefault = schema.default === null && mergedNullable ? undefined : (schema.default ?? memberNode.default)
@@ -371,7 +306,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
371
306
  const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef)
372
307
  const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef)
373
308
  if (inOneOf || inMapping) {
374
- const discriminatorValue = Object.entries(deref.discriminator.mapping ?? {}).find(([, v]) => v === childRef)?.[0]
309
+ const discriminatorValue = resolveDiscriminatorValue(deref.discriminator.mapping, childRef)
375
310
  if (discriminatorValue) {
376
311
  filteredDiscriminantValues.push({ propertyName: deref.discriminator.propertyName, value: discriminatorValue })
377
312
  }
@@ -379,7 +314,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
379
314
  }
380
315
  return true
381
316
  })
382
- .map((s) => convertSchema({ schema: s as SchemaObject }, options))
317
+ .map((s) => convertSchema({ schema: s as SchemaObject }, rawOptions))
383
318
 
384
319
  // Track where allOf-derived members end so each portion can be merged independently.
385
320
  const syntheticStart = allOfMembers.length
@@ -400,7 +335,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
400
335
  for (const key of missingRequired) {
401
336
  for (const resolved of resolvedMembers) {
402
337
  if (resolved.properties?.[key]) {
403
- allOfMembers.push(convertSchema({ schema: { properties: { [key]: resolved.properties[key] }, required: [key] } as SchemaObject }, options))
338
+ allOfMembers.push(convertSchema({ schema: { properties: { [key]: resolved.properties[key] }, required: [key] } as SchemaObject }, rawOptions))
404
339
  break
405
340
  }
406
341
  }
@@ -410,29 +345,13 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
410
345
 
411
346
  if (schema.properties) {
412
347
  const { allOf: _allOf, ...schemaWithoutAllOf } = schema
413
- allOfMembers.push(convertSchema({ schema: schemaWithoutAllOf }, options))
348
+ allOfMembers.push(convertSchema({ schema: schemaWithoutAllOf }, rawOptions))
414
349
  }
415
350
 
416
351
  // Inject a synthetic single-property object for each discriminant value collected from
417
352
  // filtered discriminator parents so that child schemas carry the narrowed literal type.
418
353
  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
- )
354
+ allOfMembers.push(createDiscriminantNode(propertyName, value))
436
355
  }
437
356
 
438
357
  // Merge consecutive anonymous object members within the synthetic portion — see `mergeAdjacentAnonymousObjects`.
@@ -451,81 +370,56 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
451
370
  * individually intersected with the shared properties node to match the OAS pattern of
452
371
  * adding common fields next to a discriminated union.
453
372
  */
454
- function convertUnion({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode {
373
+ function convertUnion({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
455
374
  const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
456
375
  const unionBase = {
457
376
  ...renderSchemaBase(schema, name, nullable, defaultValue),
458
377
  discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
459
378
  }
379
+ const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
380
+ const sharedPropertiesNode = schema.properties
381
+ ? (() => {
382
+ const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
383
+ // Strip discriminator so convertObject won't re-apply the full mapping enum.
384
+ const memberBaseSchema: SchemaObject = discriminator
385
+ ? (Object.fromEntries(Object.entries(schemaWithoutUnion).filter(([key]) => key !== 'discriminator')) as SchemaObject)
386
+ : schemaWithoutUnion
387
+ // Convert shared properties once to avoid duplicate enum naming
388
+ // (e.g. StatusEnum appearing twice and getting a numeric suffix).
389
+ return convertSchema({ schema: memberBaseSchema, name }, rawOptions)
390
+ })()
391
+ : undefined
460
392
 
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
-
393
+ if (sharedPropertiesNode || discriminator?.mapping) {
474
394
  return createSchema({
475
395
  type: 'union',
476
396
  ...unionBase,
477
397
  members: unionMembers.map((s) => {
478
398
  const ref = isReference(s) ? s.$ref : undefined
479
- const discriminatorValue = discriminator?.mapping && ref ? Object.entries(discriminator.mapping).find(([, v]) => v === ref)?.[0] : undefined
480
-
481
- let propertiesNode = sharedPropertiesNode
482
-
483
- if (discriminatorValue && discriminator) {
484
- propertiesNode = applyDiscriminatorEnum({ node: propertiesNode, propertyName: discriminator.propertyName, values: [discriminatorValue] })
399
+ const discriminatorValue = resolveDiscriminatorValue(discriminator?.mapping, ref)
400
+ const memberNode = convertSchema({ schema: s as SchemaObject }, rawOptions)
401
+
402
+ if (sharedPropertiesNode) {
403
+ const narrowedProperties =
404
+ discriminatorValue && discriminator
405
+ ? applyDiscriminatorEnum({
406
+ node: sharedPropertiesNode,
407
+ propertyName: discriminator.propertyName,
408
+ values: [discriminatorValue],
409
+ })
410
+ : sharedPropertiesNode
411
+
412
+ return createSchema({
413
+ type: 'intersection',
414
+ members: [memberNode, narrowedProperties],
415
+ })
485
416
  }
486
417
 
487
- return createSchema({
488
- type: 'intersection',
489
- members: [convertSchema({ schema: s as SchemaObject }, options), propertiesNode],
490
- })
491
- }),
492
- })
493
- }
494
-
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({
501
- type: 'union',
502
- ...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
- })
418
+ if (!discriminatorValue || !discriminator) return memberNode
525
419
 
526
420
  return createSchema({
527
421
  type: 'intersection',
528
- members: [memberNode, discriminantNode],
422
+ members: [memberNode, createDiscriminantNode(discriminator.propertyName, discriminatorValue)],
529
423
  })
530
424
  }),
531
425
  })
@@ -534,7 +428,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
534
428
  return createSchema({
535
429
  type: 'union',
536
430
  ...unionBase,
537
- members: simplifyUnionMembers(unionMembers.map((s) => convertSchema({ schema: s as SchemaObject }, options))),
431
+ members: simplifyUnionMembers(unionMembers.map((s) => convertSchema({ schema: s as SchemaObject }, rawOptions))),
538
432
  })
539
433
  }
540
434
 
@@ -573,13 +467,13 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
573
467
  * Returns `undefined` when the format should fall through to string handling
574
468
  * (i.e. `format: 'date-time'` with `dateType: false`).
575
469
  */
576
- function convertFormat({ schema, name, nullable, defaultValue, mergedOptions }: SchemaContext): SchemaNode | undefined {
470
+ function convertFormat({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode | undefined {
577
471
  const base = renderSchemaBase(schema, name, nullable, defaultValue)
578
472
 
579
473
  // int64 is option-dependent so it can't live in the static formatMap.
580
474
  if (schema.format === 'int64') {
581
475
  return createSchema({
582
- type: mergedOptions.integerType === 'bigint' ? 'bigint' : 'integer',
476
+ type: options.integerType === 'bigint' ? 'bigint' : 'integer',
583
477
  primitive: 'integer',
584
478
  ...base,
585
479
  min: schema.minimum,
@@ -591,7 +485,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
591
485
 
592
486
  // date-time / date / time are option-dependent and can't live in the static formatMap.
593
487
  if (schema.format === 'date-time' || schema.format === 'date' || schema.format === 'time') {
594
- const dateType = getDateType(mergedOptions, schema.format)
488
+ const dateType = getDateType(options, schema.format)
595
489
  if (!dateType) return undefined // dateType: false → fall through to string
596
490
 
597
491
  if (dateType.type === 'datetime') {
@@ -625,13 +519,10 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
625
519
  * - Numeric and boolean enums require a const-map representation because most generators cannot
626
520
  * use string-enum syntax for non-string values.
627
521
  */
628
- function convertEnum({ schema, name, nullable, type, options }: SchemaContext): SchemaNode {
522
+ function convertEnum({ schema, name, nullable, type, rawOptions }: SchemaContext): SchemaNode {
629
523
  // Malformed schema: `{ type: 'array', enum: [...] }` — normalize by moving the enum into items.
630
524
  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)
525
+ return convertSchema({ schema: normalizeArrayEnum(schema), name }, rawOptions)
635
526
  }
636
527
 
637
528
  // `null` in enum values is the OAS 3.0 convention for a nullable enum.
@@ -657,53 +548,26 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
657
548
 
658
549
  // x-enumNames / x-enum-varnames: named variants with explicit labels take priority.
659
550
  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)
551
+ if (extensionKey || enumPrimitive === 'number' || enumPrimitive === 'integer' || enumPrimitive === 'boolean') {
552
+ const enumType = (enumPrimitive === 'number' || enumPrimitive === 'integer' ? 'number' : enumPrimitive === 'boolean' ? 'boolean' : 'string') as
553
+ | 'number'
554
+ | 'boolean'
555
+ | 'string'
556
+ const sourceValues = extensionKey
557
+ ? [...new Set((schema as Record<string, unknown>)[extensionKey] as Array<string | number>)]
558
+ : [...new Set(filteredValues)]
669
559
 
670
560
  return createSchema({
671
561
  ...enumBase,
672
562
  enumType,
673
- namedEnumValues: uniqueNames.map((label, index) => ({
563
+ namedEnumValues: sourceValues.map((label, index) => ({
674
564
  name: String(label),
675
- value: filteredValues[index] ?? label,
565
+ value: extensionKey ? (filteredValues[index] ?? label) : label,
676
566
  format: enumType,
677
567
  })),
678
568
  })
679
569
  }
680
570
 
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,
703
- })),
704
- })
705
- }
706
-
707
571
  // Plain string enum (default path).
708
572
  return createSchema({
709
573
  ...enumBase,
@@ -723,48 +587,8 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
723
587
  * - not required + not nullable → `optional: true`
724
588
  * - not required + nullable → `nullish: true`
725
589
  */
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
590
 
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
-
764
- return propNode
765
- }
766
-
767
- function convertObject({ schema, name, nullable, defaultValue, options, mergedOptions }: SchemaContext): SchemaNode {
591
+ function convertObject({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): SchemaNode {
768
592
  const properties: Array<PropertyNode> = schema.properties
769
593
  ? Object.entries(schema.properties).map(([propName, propSchema]) => {
770
594
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required
@@ -772,12 +596,12 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
772
596
  const propNullable = isNullable(resolvedPropSchema)
773
597
 
774
598
  const childName = resolveChildName(name, propName)
775
- const propNode = convertSchema({ schema: resolvedPropSchema, name: childName }, options)
776
- let schemaNode = applyEnumName(propNode, name, propName, mergedOptions.enumSuffix)
599
+ const propNode = convertSchema({ schema: resolvedPropSchema, name: childName }, rawOptions)
600
+ let schemaNode = applyEnumName(propNode, name, propName, options.enumSuffix)
777
601
 
778
602
  const tupleNode = narrowSchema(schemaNode, 'tuple')
779
603
  if (tupleNode?.items) {
780
- const namedItems = tupleNode.items.map((item) => applyEnumName(item, name, propName, mergedOptions.enumSuffix))
604
+ const namedItems = tupleNode.items.map((item) => applyEnumName(item, name, propName, options.enumSuffix))
781
605
  if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
782
606
  schemaNode = { ...tupleNode, items: namedItems }
783
607
  }
@@ -799,11 +623,11 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
799
623
  if (additionalProperties === true) {
800
624
  additionalPropertiesNode = true
801
625
  } else if (additionalProperties && Object.keys(additionalProperties).length > 0) {
802
- additionalPropertiesNode = convertSchema({ schema: additionalProperties as SchemaObject }, options)
626
+ additionalPropertiesNode = convertSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
803
627
  } else if (additionalProperties === false) {
804
628
  additionalPropertiesNode = undefined
805
629
  } else if (additionalProperties) {
806
- additionalPropertiesNode = createSchema({ type: resolveTypeOption(mergedOptions.unknownType) })
630
+ additionalPropertiesNode = createSchema({ type: resolveTypeOption(options.unknownType) })
807
631
  }
808
632
 
809
633
  const rawPatternProperties = 'patternProperties' in schema ? schema.patternProperties : undefined
@@ -813,8 +637,8 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
813
637
  Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [
814
638
  pattern,
815
639
  patternSchema === true || (typeof patternSchema === 'object' && Object.keys(patternSchema).length === 0)
816
- ? createSchema({ type: resolveTypeOption(mergedOptions.unknownType) })
817
- : convertSchema({ schema: patternSchema as SchemaObject }, options),
640
+ ? createSchema({ type: resolveTypeOption(options.unknownType) })
641
+ : convertSchema({ schema: patternSchema as SchemaObject }, rawOptions),
818
642
  ]),
819
643
  )
820
644
  : undefined
@@ -833,7 +657,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
833
657
  if (isDiscriminator(schema) && schema.discriminator.mapping) {
834
658
  const discPropName = schema.discriminator.propertyName
835
659
  const values = Object.keys(schema.discriminator.mapping)
836
- const enumName = name ? resolveEnumPropName(name, discPropName, mergedOptions.enumSuffix) : undefined
660
+ const enumName = name ? resolveEnumPropName(name, discPropName, options.enumSuffix) : undefined
837
661
  return applyDiscriminatorEnum({ node: objectNode, propertyName: discPropName, values, enumName })
838
662
  }
839
663
 
@@ -848,9 +672,9 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
848
672
  * a rest element of type `any` is emitted to match JSON Schema semantics (absent `items`
849
673
  * means additional items are allowed).
850
674
  */
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' })
675
+ function convertTuple({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
676
+ const tupleItems = (schema.prefixItems ?? []).map((item) => convertSchema({ schema: item as SchemaObject }, rawOptions))
677
+ const rest = schema.items ? convertSchema({ schema: schema.items as SchemaObject }, rawOptions) : createSchema({ type: 'any' })
854
678
 
855
679
  return createSchema({
856
680
  type: 'tuple',
@@ -869,12 +693,12 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
869
693
  * When the items schema is an inline enum, a name derived from the parent array's name and
870
694
  * `enumSuffix` is forwarded so generators can emit a named enum declaration.
871
695
  */
872
- function convertArray({ schema, name, nullable, defaultValue, options, mergedOptions }: SchemaContext): SchemaNode {
696
+ function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): SchemaNode {
873
697
  const rawItems = schema.items as SchemaObject | undefined
874
698
  // When the items schema contains an inline enum, derive a named identifier
875
699
  // 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)] : []
700
+ const itemName = rawItems?.enum?.length && name ? resolveEnumPropName(undefined, name, options.enumSuffix) : undefined
701
+ const items = rawItems ? [convertSchema({ schema: rawItems, name: itemName }, rawOptions)] : []
878
702
 
879
703
  return createSchema({
880
704
  type: 'array',
@@ -958,13 +782,13 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
958
782
  * 10. Object / array / tuple / scalar by `type`
959
783
  * 11. Empty schema fallback (`emptySchemaType` option)
960
784
  */
961
- function convertSchema({ schema, name }: { schema: SchemaObject; name?: string }, options?: Partial<ParserOptions>): SchemaNode {
962
- const mergedOptions: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...options }
785
+ function convertSchema({ schema, name }: { schema: SchemaObject; name?: string }, rawOptions?: Partial<ParserOptions>): SchemaNode {
786
+ const options: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...rawOptions }
963
787
  // Flatten keyword-only allOf fragments (no $ref, no structural keys) into the parent
964
788
  // schema before parsing, so simple annotation patterns don't produce needless intersections.
965
789
  const flattenedSchema = flattenSchema(schema)
966
790
  if (flattenedSchema && flattenedSchema !== schema) {
967
- return convertSchema({ schema: flattenedSchema, name }, options)
791
+ return convertSchema({ schema: flattenedSchema, name }, rawOptions)
968
792
  }
969
793
 
970
794
  const nullable = isNullable(schema) || undefined
@@ -972,7 +796,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
972
796
  // Normalize OAS 3.1 multi-type array to a single type string for the dispatch below.
973
797
  const type = Array.isArray(schema.type) ? schema.type[0] : schema.type
974
798
 
975
- const ctx: SchemaContext = { schema, name, nullable, defaultValue, type, options, mergedOptions }
799
+ const ctx: SchemaContext = { schema, name, nullable, defaultValue, type, rawOptions, options }
976
800
 
977
801
  // $ref — pointer to another definition.
978
802
  // In OAS 3.0 siblings of $ref are technically ignored, but Kubb intentionally preserves them
@@ -1011,7 +835,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
1011
835
  if (nonNullTypes.length > 1) {
1012
836
  return createSchema({
1013
837
  type: 'union',
1014
- members: nonNullTypes.map((t) => convertSchema({ schema: { ...schema, type: t } as SchemaObject, name }, options)),
838
+ members: nonNullTypes.map((t) => convertSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
1015
839
  ...renderSchemaBase(schema, name, arrayNullable, defaultValue),
1016
840
  })
1017
841
  }
@@ -1039,7 +863,7 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
1039
863
  if (type === 'boolean') return convertBoolean(ctx)
1040
864
  if (type === 'null') return convertNull(ctx)
1041
865
 
1042
- const emptyType = resolveTypeOption(mergedOptions.emptySchemaType)
866
+ const emptyType = resolveTypeOption(options.emptySchemaType)
1043
867
  return createSchema({ type: emptyType as ScalarSchemaType, name, title: schema.title, description: schema.description })
1044
868
  }
1045
869
 
@@ -1163,36 +987,11 @@ export function createOasParser(oas: Oas, { contentType }: OasParserOptions = {}
1163
987
  return createRoot({ schemas, operations })
1164
988
  }
1165
989
 
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
- }
990
+ const resolveRefs = (
991
+ node: SchemaNode,
992
+ resolveName: (ref: string) => string | undefined,
993
+ resolveEnumName?: (name: string) => string | undefined,
994
+ ): SchemaNode => resolveRefsNode({ node, nameMapping, resolveName, resolveEnumName })
1196
995
 
1197
996
  return {
1198
997
  parse,