@kubb/adapter-oas 5.0.0-alpha.10 → 5.0.0-alpha.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/adapter-oas",
3
- "version": "5.0.0-alpha.10",
3
+ "version": "5.0.0-alpha.12",
4
4
  "description": "OpenAPI / Swagger adapter for Kubb — converts OAS input into a @kubb/ast RootNode.",
5
5
  "keywords": [
6
6
  "openapi",
@@ -37,7 +37,7 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "@kubb/fabric-core": "0.14.0",
40
- "@redocly/openapi-core": "^2.24.0",
40
+ "@redocly/openapi-core": "^2.24.1",
41
41
  "@stoplight/yaml": "^4.3.0",
42
42
  "fflate": "^0.8.2",
43
43
  "jsonpointer": "^5.0.1",
@@ -46,8 +46,8 @@
46
46
  "openapi-types": "^12.1.3",
47
47
  "remeda": "^2.33.6",
48
48
  "swagger2openapi": "^7.0.8",
49
- "@kubb/ast": "5.0.0-alpha.10",
50
- "@kubb/core": "5.0.0-alpha.10"
49
+ "@kubb/ast": "5.0.0-alpha.12",
50
+ "@kubb/core": "5.0.0-alpha.12"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/swagger2openapi": "^7.0.4",
package/src/adapter.ts CHANGED
@@ -2,6 +2,7 @@ import path from 'node:path'
2
2
  import { createRoot } from '@kubb/ast'
3
3
  import type { AdapterSource } from '@kubb/core'
4
4
  import { createAdapter } from '@kubb/core'
5
+ import { DEFAULT_PARSER_OPTIONS } from './constants.ts'
5
6
  import { resolveServerUrl } from './oas/resolveServerUrl.ts'
6
7
  import { parseFromConfig } from './oas/utils.ts'
7
8
  import { createOasParser } from './parser.ts'
@@ -36,11 +37,12 @@ export const adapterOas = createAdapter<OasAdapter>((options) => {
36
37
  serverIndex,
37
38
  serverVariables,
38
39
  discriminator = 'strict',
39
- collisionDetection = false,
40
- dateType = 'string',
41
- integerType = 'number',
42
- unknownType = 'any',
43
- emptySchemaType = unknownType,
40
+ collisionDetection = true,
41
+ dateType = DEFAULT_PARSER_OPTIONS.dateType,
42
+ integerType = DEFAULT_PARSER_OPTIONS.integerType,
43
+ unknownType = DEFAULT_PARSER_OPTIONS.unknownType,
44
+ enumSuffix = DEFAULT_PARSER_OPTIONS.enumSuffix,
45
+ emptySchemaType = unknownType || DEFAULT_PARSER_OPTIONS.emptySchemaType,
44
46
  } = options
45
47
 
46
48
  // Mutable Map shared between `options` and each `parse()` call.
@@ -61,6 +63,7 @@ export const adapterOas = createAdapter<OasAdapter>((options) => {
61
63
  integerType,
62
64
  unknownType,
63
65
  emptySchemaType,
66
+ enumSuffix,
64
67
  nameMapping,
65
68
  },
66
69
  getImports(node, resolve) {
@@ -91,7 +94,7 @@ export const adapterOas = createAdapter<OasAdapter>((options) => {
91
94
  nameMapping.set(key, value)
92
95
  }
93
96
 
94
- const root = parser.parse({ dateType, integerType, unknownType, emptySchemaType })
97
+ const root = parser.parse({ dateType, integerType, unknownType, emptySchemaType, enumSuffix })
95
98
 
96
99
  return createRoot({
97
100
  ...root,
package/src/constants.ts CHANGED
@@ -1,7 +1,17 @@
1
1
  import type { SchemaType } from '@kubb/ast/types'
2
2
  import type { HttpMethods as OASHttpMethods } from 'oas/types'
3
+ import type { ParserOptions } from './types.ts'
3
4
 
4
- // ─── Merge defaults ────────────────────────────────────────────────────────────
5
+ /**
6
+ * Default values for all `Options` fields.
7
+ */
8
+ export const DEFAULT_PARSER_OPTIONS = {
9
+ dateType: 'string',
10
+ integerType: 'number',
11
+ unknownType: 'any',
12
+ emptySchemaType: 'any',
13
+ enumSuffix: 'enum',
14
+ } as const satisfies ParserOptions
5
15
 
6
16
  /**
7
17
  * OpenAPI version string written into merged document stubs.
@@ -18,8 +28,6 @@ export const MERGE_DEFAULT_TITLE = 'Merged API' as const
18
28
  */
19
29
  export const MERGE_DEFAULT_VERSION = '1.0.0' as const
20
30
 
21
- // ─── Schema analysis ───────────────────────────────────────────────────────────
22
-
23
31
  /**
24
32
  * JSON Schema keywords that indicate structural composition.
25
33
  * A schema fragment containing any of these keys must not be inlined into its
@@ -93,8 +101,6 @@ export const enumExtensionKeys = ['x-enumNames', 'x-enum-varnames'] as const
93
101
  */
94
102
  export const SCALAR_PRIMITIVE_TYPES = new Set(['string', 'number', 'integer', 'bigint', 'boolean'] as const)
95
103
 
96
- // ─── HTTP ──────────────────────────────────────────────────────────────────────
97
-
98
104
  /**
99
105
  * Canonical HTTP method names for the Kubb OAS layer.
100
106
  * Keys are uppercase (used in generated code); values are the lowercase strings
package/src/parser.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { pascalCase, URLPath } from '@internals/utils'
1
+ import { getUniqueName, pascalCase, URLPath } from '@internals/utils'
2
2
  import { createOperation, createParameter, createProperty, createResponse, createRoot, createSchema, narrowSchema, schemaTypes, transform } from '@kubb/ast'
3
3
  import type {
4
4
  ArraySchemaNode,
@@ -27,10 +27,11 @@ import type {
27
27
  TimeSchemaNode,
28
28
  UnionSchemaNode,
29
29
  } from '@kubb/ast/types'
30
- import { enumExtensionKeys, formatMap, knownMediaTypes } from './constants.ts'
30
+ import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, formatMap, knownMediaTypes } from './constants.ts'
31
31
  import type { Oas } from './oas/Oas.ts'
32
32
  import type { contentType, Operation, ReferenceObject, SchemaObject } from './oas/types.ts'
33
33
  import { flattenSchema, isDiscriminator, isNullable, isReference } from './oas/utils.ts'
34
+ import type { ParserOptions } from './types.ts'
34
35
  import { applyDiscriminatorEnum, extractRefName, mergeAdjacentAnonymousObjects, simplifyUnionMembers } from './utils.ts'
35
36
 
36
37
  /**
@@ -54,14 +55,16 @@ type DateTimeNodeByDateType = {
54
55
  /**
55
56
  * Resolves the AST node produced by `format: 'date-time'` based on the `dateType` option.
56
57
  */
57
- type ResolveDateTimeNode<TDateType extends Options['dateType']> = DateTimeNodeByDateType[TDateType extends keyof DateTimeNodeByDateType ? TDateType : 'string']
58
+ type ResolveDateTimeNode<TDateType extends ParserOptions['dateType']> = DateTimeNodeByDateType[TDateType extends keyof DateTimeNodeByDateType
59
+ ? TDateType
60
+ : 'string']
58
61
 
59
62
  /**
60
63
  * Single source of truth: ordered list of `[shape, SchemaNode]` pairs.
61
64
  * `InferSchemaNode` walks this tuple in order and returns the node type of the first matching entry.
62
65
  * Parameterized over `TDateType` so `format: 'date-time'` resolves to the correct node based on the option.
63
66
  */
64
- type SchemaNodeMap<TDateType extends Options['dateType'] = Options['dateType']> = [
67
+ type SchemaNodeMap<TDateType extends ParserOptions['dateType'] = 'string'> = [
65
68
  [{ $ref: string }, RefSchemaNode],
66
69
  // allOf with sibling `properties` always produces an intersection (shared props are appended as a member).
67
70
  [{ allOf: ReadonlyArray<unknown>; properties: object }, IntersectionSchemaNode],
@@ -105,7 +108,7 @@ type SchemaNodeMap<TDateType extends Options['dateType'] = Options['dateType']>
105
108
 
106
109
  export type InferSchemaNode<
107
110
  TSchema extends SchemaObject,
108
- TDateType extends Options['dateType'] = Options['dateType'],
111
+ TDateType extends ParserOptions['dateType'] = 'string',
109
112
  TEntries extends ReadonlyArray<[object, SchemaNode]> = SchemaNodeMap<TDateType>,
110
113
  > = TEntries extends [infer TEntry extends [object, SchemaNode], ...infer TRest extends ReadonlyArray<[object, SchemaNode]>]
111
114
  ? TSchema extends TEntry[0]
@@ -113,32 +116,6 @@ export type InferSchemaNode<
113
116
  : InferSchemaNode<TSchema, TDateType, TRest>
114
117
  : SchemaNode
115
118
 
116
- /**
117
- * Controls how various OAS constructs are mapped to Kubb AST nodes.
118
- */
119
- export type Options = {
120
- /**
121
- * How `format: 'date-time'` schemas are represented. `false` falls through to a plain string.
122
- */
123
- dateType: false | 'string' | 'stringOffset' | 'stringLocal' | 'date'
124
- /**
125
- * Whether `type: 'integer'` and `format: 'int64'` produce `number` or `bigint` nodes.
126
- */
127
- integerType?: 'number' | 'bigint'
128
- /**
129
- * AST type used when no schema type can be inferred.
130
- */
131
- unknownType: 'any' | 'unknown' | 'void'
132
- /**
133
- * AST type used for completely empty schemas (`{}`).
134
- */
135
- emptySchemaType: 'any' | 'unknown' | 'void'
136
- /**
137
- * Suffix appended to derived enum names when building property schema names.
138
- */
139
- enumSuffix: string
140
- }
141
-
142
119
  /**
143
120
  * Construction-time options for `createOasParser`.
144
121
  */
@@ -147,17 +124,6 @@ export type OasParserOptions = {
147
124
  collisionDetection?: boolean
148
125
  }
149
126
 
150
- /**
151
- * Default values for all `Options` fields.
152
- */
153
- const DEFAULT_OPTIONS = {
154
- dateType: 'string',
155
- integerType: 'number',
156
- unknownType: 'any',
157
- emptySchemaType: 'any',
158
- enumSuffix: 'enum',
159
- } as const satisfies Options
160
-
161
127
  /**
162
128
  * Looks up the Kubb `SchemaType` for a given OAS `format` string.
163
129
  * Returns `undefined` for formats not in `formatMap` (e.g. `int64`, `date-time`),
@@ -199,8 +165,8 @@ type SchemaContext = {
199
165
  * Normalized single type string (first element when OAS 3.1 multi-type array).
200
166
  */
201
167
  type: string | undefined
202
- options: Partial<Options> | undefined
203
- mergedOptions: Options
168
+ options: Partial<ParserOptions> | undefined
169
+ mergedOptions: ParserOptions
204
170
  }
205
171
 
206
172
  /**
@@ -211,11 +177,11 @@ export type OasParser = {
211
177
  * Converts an OpenAPI/Swagger spec (wrapped in a Kubb `Oas` instance) into
212
178
  * a `RootNode` — the top-level node of the `@kubb/ast` tree.
213
179
  */
214
- parse: <TOptions extends Partial<Options> = object>(options?: TOptions) => RootNode
215
- convertSchema: <TFormat extends string, TSchema extends SchemaObject & { format?: TFormat }, TOptions extends Partial<Options> = object>(
180
+ parse: <TOptions extends Partial<ParserOptions> = object>(options?: TOptions) => RootNode
181
+ convertSchema: <TFormat extends string, TSchema extends SchemaObject & { format?: TFormat }, TOptions extends Partial<ParserOptions> = object>(
216
182
  params: { schema: TSchema; name?: string },
217
183
  options?: TOptions,
218
- ) => InferSchemaNode<TSchema, TOptions extends { dateType: Options['dateType'] } ? TOptions['dateType'] : (typeof DEFAULT_OPTIONS)['dateType']>
184
+ ) => InferSchemaNode<TSchema, TOptions extends { dateType: ParserOptions['dateType'] } ? TOptions['dateType'] : 'string'>
219
185
  /**
220
186
  * Walks `node` and replaces each `ref` value with the name returned by
221
187
  * `resolveName`. The callback receives the full `$ref` path (e.g. `#/components/schemas/Order`)
@@ -260,6 +226,14 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
260
226
  // e.g., { '#/components/schemas/Order': 'OrderSchema', '#/components/responses/Product': 'ProductResponse' }
261
227
  const { schemas: schemaObjects, nameMapping } = oas.getSchemas({ contentType, collisionDetection })
262
228
 
229
+ // Legacy enum name deduplication: tracks used enum names and appends numeric suffixes
230
+ // (e.g. ParamsStatusEnum, ParamsStatusEnum2) when collisionDetection is disabled.
231
+ const usedEnumNames: Record<string, number> = {}
232
+
233
+ // Only apply legacy naming when collisionDetection is explicitly false.
234
+ // When undefined (e.g. direct parser usage without adapter), use the default (new) behavior.
235
+ const isLegacyNaming = collisionDetection === false
236
+
263
237
  /**
264
238
  * Maps an `'any' | 'unknown' | 'void'` option string to the corresponding `SchemaType` constant.
265
239
  * Used for both `unknownType` (unannotated schemas) and `emptySchemaType` (empty `{}` schemas).
@@ -275,7 +249,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
275
249
  * Returns `undefined` when `dateType` is `false`, meaning the format should fall through to `string`.
276
250
  */
277
251
  function getDateType(
278
- options: Options,
252
+ options: ParserOptions,
279
253
  format: 'date-time' | 'date' | 'time',
280
254
  ): { type: 'datetime'; offset?: boolean; local?: boolean } | { type: 'date' | 'time'; representation: 'date' | 'string' } | undefined {
281
255
  if (!options.dateType) {
@@ -307,7 +281,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
307
281
  * Shared metadata fields included in every `createSchema` call.
308
282
  * Centralizes the common properties so sub-handlers don't repeat them.
309
283
  */
310
- function buildSchemaBase(schema: SchemaObject, name: string | undefined, nullable: true | undefined, defaultValue: unknown) {
284
+ function renderSchemaBase(schema: SchemaObject, name: string | undefined, nullable: true | undefined, defaultValue: unknown) {
311
285
  return {
312
286
  name,
313
287
  nullable,
@@ -444,7 +418,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
444
418
  return createSchema({
445
419
  type: 'intersection',
446
420
  members: [...allOfMembers.slice(0, syntheticStart), ...mergeAdjacentAnonymousObjects(allOfMembers.slice(syntheticStart))],
447
- ...buildSchemaBase(schema, name, nullable, defaultValue),
421
+ ...renderSchemaBase(schema, name, nullable, defaultValue),
448
422
  })
449
423
  }
450
424
 
@@ -459,7 +433,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
459
433
  function convertUnion({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode {
460
434
  const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
461
435
  const unionBase = {
462
- ...buildSchemaBase(schema, name, nullable, defaultValue),
436
+ ...renderSchemaBase(schema, name, nullable, defaultValue),
463
437
  discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
464
438
  }
465
439
 
@@ -472,6 +446,10 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
472
446
  ? (Object.fromEntries(Object.entries(schemaWithoutUnion).filter(([key]) => key !== 'discriminator')) as SchemaObject)
473
447
  : schemaWithoutUnion
474
448
 
449
+ // Convert shared properties once to avoid duplicate enum naming
450
+ // (e.g. StatusEnum appearing twice and getting a numeric suffix).
451
+ const sharedPropertiesNode = convertSchema({ schema: memberBaseSchema, name: isLegacyNaming ? undefined : name }, options)
452
+
475
453
  return createSchema({
476
454
  type: 'union',
477
455
  ...unionBase,
@@ -479,7 +457,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
479
457
  const ref = isReference(s) ? s.$ref : undefined
480
458
  const discriminatorValue = discriminator?.mapping && ref ? Object.entries(discriminator.mapping).find(([, v]) => v === ref)?.[0] : undefined
481
459
 
482
- let propertiesNode = convertSchema({ schema: memberBaseSchema, name }, options)
460
+ let propertiesNode = sharedPropertiesNode
483
461
 
484
462
  if (discriminatorValue && discriminator) {
485
463
  propertiesNode = applyDiscriminatorEnum({ node: propertiesNode, propertyName: discriminator.propertyName, values: [discriminatorValue] })
@@ -525,7 +503,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
525
503
  type: 'enum',
526
504
  primitive: constPrimitive,
527
505
  enumValues: [constValue as string | number | boolean],
528
- ...buildSchemaBase(schema, name, nullable, defaultValue),
506
+ ...renderSchemaBase(schema, name, nullable, defaultValue),
529
507
  })
530
508
  }
531
509
 
@@ -535,7 +513,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
535
513
  * (i.e. `format: 'date-time'` with `dateType: false`).
536
514
  */
537
515
  function convertFormat({ schema, name, nullable, defaultValue, mergedOptions }: SchemaContext): SchemaNode | undefined {
538
- const base = buildSchemaBase(schema, name, nullable, defaultValue)
516
+ const base = renderSchemaBase(schema, name, nullable, defaultValue)
539
517
 
540
518
  // int64 is option-dependent so it can't live in the static formatMap.
541
519
  if (schema.format === 'int64') {
@@ -684,17 +662,64 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
684
662
  * - not required + not nullable → `optional: true`
685
663
  * - not required + nullable → `nullish: true`
686
664
  */
665
+ /**
666
+ * Builds the propagation name for a child property during recursive schema conversion.
667
+ *
668
+ * - **Legacy naming** (`isLegacyNaming`): only the immediate property key is used
669
+ * (e.g. `Params` for property `params`), keeping nested names short.
670
+ * - **Default naming**: the parent name is prepended so the full path is encoded
671
+ * (e.g. `OrderParams` when parent is `Order`).
672
+ */
673
+ function resolveChildName(parentName: string | undefined, propName: string): string | undefined {
674
+ if (isLegacyNaming) {
675
+ return pascalCase(propName)
676
+ }
677
+ return parentName ? pascalCase([parentName, propName].join(' ')) : undefined
678
+ }
679
+
680
+ /**
681
+ * Derives the final name for an enum property schema node.
682
+ *
683
+ * The raw name always includes the enum suffix (e.g. `StatusEnum`).
684
+ * In legacy mode an additional deduplication step appends a numeric suffix
685
+ * when the same name has already been used (e.g. `ParamsStatusEnum2`).
686
+ */
687
+ function resolveEnumPropName(parentName: string | undefined, propName: string, enumSuffix: string): string {
688
+ const raw = pascalCase([parentName, propName, enumSuffix].filter(Boolean).join(' '))
689
+ return isLegacyNaming ? getUniqueName(raw, usedEnumNames) : raw
690
+ }
691
+
692
+ /**
693
+ * Given a freshly-converted property schema, returns the node with a correct
694
+ * `name` attached — or stripped — depending on whether the node is a named
695
+ * enum, a boolean const-enum (always inlined), or a regular schema.
696
+ */
697
+ function applyEnumName(propNode: SchemaNode, parentName: string | undefined, propName: string, enumSuffix: string): SchemaNode {
698
+ const enumNode = narrowSchema(propNode, 'enum')
699
+
700
+ // Boolean-primitive enum nodes (e.g. `const: false`) are always inlined as
701
+ // literal types and must not receive a named identifier.
702
+ if (enumNode?.primitive === 'boolean') {
703
+ return { ...propNode, name: undefined }
704
+ }
705
+
706
+ if (enumNode) {
707
+ return { ...propNode, name: resolveEnumPropName(parentName, propName, enumSuffix) }
708
+ }
709
+
710
+ return propNode
711
+ }
712
+
687
713
  function convertObject({ schema, name, nullable, defaultValue, options, mergedOptions }: SchemaContext): SchemaNode {
688
714
  const properties: Array<PropertyNode> = schema.properties
689
715
  ? Object.entries(schema.properties).map(([propName, propSchema]) => {
690
716
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required
691
717
  const resolvedPropSchema = propSchema as SchemaObject
692
718
  const propNullable = isNullable(resolvedPropSchema)
693
- const basePropName = name ? pascalCase([name, propName].join(' ')) : undefined
694
- const propNode = convertSchema({ schema: resolvedPropSchema, name: basePropName }, options)
695
- const isEnumNode = !!narrowSchema(propNode, 'enum')
696
- const derivedPropName = isEnumNode && name ? pascalCase([name, propName, mergedOptions.enumSuffix].filter(Boolean).join(' ')) : basePropName
697
- const schemaNode = isEnumNode && derivedPropName !== basePropName ? { ...propNode, name: derivedPropName } : propNode
719
+
720
+ const childName = resolveChildName(name, propName)
721
+ const propNode = convertSchema({ schema: resolvedPropSchema, name: childName }, options)
722
+ const schemaNode = applyEnumName(propNode, name, propName, mergedOptions.enumSuffix)
698
723
 
699
724
  return createProperty({
700
725
  name: propName,
@@ -740,7 +765,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
740
765
  properties,
741
766
  additionalProperties: additionalPropertiesNode,
742
767
  patternProperties,
743
- ...buildSchemaBase(schema, name, nullable, defaultValue),
768
+ ...renderSchemaBase(schema, name, nullable, defaultValue),
744
769
  })
745
770
 
746
771
  // When a discriminator is present, replace the discriminator property's schema
@@ -748,7 +773,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
748
773
  if (isDiscriminator(schema) && schema.discriminator.mapping) {
749
774
  const discPropName = schema.discriminator.propertyName
750
775
  const values = Object.keys(schema.discriminator.mapping)
751
- const enumName = name ? pascalCase([name, discPropName, mergedOptions.enumSuffix].filter(Boolean).join(' ')) : undefined
776
+ const enumName = name ? resolveEnumPropName(name, discPropName, mergedOptions.enumSuffix) : undefined
752
777
  return applyDiscriminatorEnum({ node: objectNode, propertyName: discPropName, values, enumName })
753
778
  }
754
779
 
@@ -772,7 +797,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
772
797
  rest,
773
798
  min: schema.minItems,
774
799
  max: schema.maxItems,
775
- ...buildSchemaBase(schema, name, nullable, defaultValue),
800
+ ...renderSchemaBase(schema, name, nullable, defaultValue),
776
801
  })
777
802
  }
778
803
 
@@ -784,9 +809,9 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
784
809
  */
785
810
  function convertArray({ schema, name, nullable, defaultValue, options, mergedOptions }: SchemaContext): SchemaNode {
786
811
  const rawItems = schema.items as SchemaObject | undefined
787
- // When the array items schema contains an inline enum, derive a name from the parent
788
- // array's name + enumSuffix so generators can emit a named enum declaration.
789
- const itemName = rawItems?.enum?.length && name ? pascalCase([name, mergedOptions.enumSuffix].join(' ')) : undefined
812
+ // When the items schema contains an inline enum, derive a named identifier
813
+ // so generators can emit a standalone enum declaration.
814
+ const itemName = rawItems?.enum?.length && name ? resolveEnumPropName(undefined, name, mergedOptions.enumSuffix) : undefined
790
815
  const items = rawItems ? [convertSchema({ schema: rawItems, name: itemName }, options)] : []
791
816
 
792
817
  return createSchema({
@@ -796,7 +821,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
796
821
  min: schema.minItems,
797
822
  max: schema.maxItems,
798
823
  unique: schema.uniqueItems ?? undefined,
799
- ...buildSchemaBase(schema, name, nullable, defaultValue),
824
+ ...renderSchemaBase(schema, name, nullable, defaultValue),
800
825
  })
801
826
  }
802
827
 
@@ -810,7 +835,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
810
835
  min: schema.minLength,
811
836
  max: schema.maxLength,
812
837
  pattern: schema.pattern,
813
- ...buildSchemaBase(schema, name, nullable, defaultValue),
838
+ ...renderSchemaBase(schema, name, nullable, defaultValue),
814
839
  })
815
840
  }
816
841
 
@@ -825,7 +850,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
825
850
  max: schema.maximum,
826
851
  exclusiveMinimum: typeof schema.exclusiveMinimum === 'number' ? schema.exclusiveMinimum : undefined,
827
852
  exclusiveMaximum: typeof schema.exclusiveMaximum === 'number' ? schema.exclusiveMaximum : undefined,
828
- ...buildSchemaBase(schema, name, nullable, defaultValue),
853
+ ...renderSchemaBase(schema, name, nullable, defaultValue),
829
854
  })
830
855
  }
831
856
 
@@ -836,7 +861,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
836
861
  return createSchema({
837
862
  type: 'boolean',
838
863
  primitive: 'boolean',
839
- ...buildSchemaBase(schema, name, nullable, defaultValue),
864
+ ...renderSchemaBase(schema, name, nullable, defaultValue),
840
865
  })
841
866
  }
842
867
 
@@ -871,8 +896,8 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
871
896
  * 10. Object / array / tuple / scalar by `type`
872
897
  * 11. Empty schema fallback (`emptySchemaType` option)
873
898
  */
874
- function convertSchema({ schema, name }: { schema: SchemaObject; name?: string }, options?: Partial<Options>): SchemaNode {
875
- const mergedOptions: Options = { ...DEFAULT_OPTIONS, ...options }
899
+ function convertSchema({ schema, name }: { schema: SchemaObject; name?: string }, options?: Partial<ParserOptions>): SchemaNode {
900
+ const mergedOptions: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...options }
876
901
  // Flatten keyword-only allOf fragments (no $ref, no structural keys) into the parent
877
902
  // schema before parsing, so simple annotation patterns don't produce needless intersections.
878
903
  const flattenedSchema = flattenSchema(schema)
@@ -911,7 +936,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
911
936
 
912
937
  // OAS 3.1: `contentMediaType: 'application/octet-stream'` on a string schema signals binary data.
913
938
  if (schema.type === 'string' && schema.contentMediaType === 'application/octet-stream') {
914
- return createSchema({ type: 'blob', primitive: 'string', ...buildSchemaBase(schema, name, nullable, defaultValue) })
939
+ return createSchema({ type: 'blob', primitive: 'string', ...renderSchemaBase(schema, name, nullable, defaultValue) })
915
940
  }
916
941
 
917
942
  // OAS 3.1: `type` may be an array — e.g. `["string", "integer", "null"]`.
@@ -925,7 +950,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
925
950
  return createSchema({
926
951
  type: 'union',
927
952
  members: nonNullTypes.map((t) => convertSchema({ schema: { ...schema, type: t } as SchemaObject, name }, options)),
928
- ...buildSchemaBase(schema, name, arrayNullable, defaultValue),
953
+ ...renderSchemaBase(schema, name, arrayNullable, defaultValue),
929
954
  })
930
955
  }
931
956
  }
@@ -960,7 +985,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
960
985
  * Converts a single dereferenced OAS parameter object into a `ParameterNode`.
961
986
  * When the parameter has no `schema` or its schema is a `$ref`, falls back to `unknownType`.
962
987
  */
963
- function parseParameter(options: Options, param: Record<string, unknown>): ParameterNode {
988
+ function parseParameter(options: ParserOptions, param: Record<string, unknown>): ParameterNode {
964
989
  const required = (param['required'] as boolean | undefined) ?? false
965
990
 
966
991
  const schema: SchemaNode =
@@ -984,11 +1009,24 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
984
1009
  * Converts an OAS `Operation` into an `OperationNode`, resolving parameters,
985
1010
  * request body, and all response codes into their AST node equivalents.
986
1011
  */
987
- function parseOperation(options: Options, oas: Oas, operation: Operation): OperationNode {
1012
+ function parseOperation(options: ParserOptions, oas: Oas, operation: Operation): OperationNode {
988
1013
  const parameters: Array<ParameterNode> = oas.getParameters(operation).map((param) => parseParameter(options, param as unknown as Record<string, unknown>))
989
1014
 
990
1015
  const requestBodySchema = oas.getRequestSchema(operation)
991
- const requestBody = requestBodySchema ? convertSchema({ schema: requestBodySchema }, options) : undefined
1016
+ const requestBodySchemaNode = requestBodySchema ? convertSchema({ schema: requestBodySchema }, options) : undefined
1017
+
1018
+ const requestBodyKeysToOmit = requestBodySchema?.properties
1019
+ ? Object.entries(requestBodySchema.properties)
1020
+ .filter(([, prop]) => !isReference(prop) && (prop as { readOnly?: boolean }).readOnly)
1021
+ .map(([key]) => key)
1022
+ : undefined
1023
+
1024
+ const requestBody = requestBodySchemaNode
1025
+ ? {
1026
+ schema: requestBodySchemaNode,
1027
+ keysToOmit: requestBodyKeysToOmit?.length ? requestBodyKeysToOmit : undefined,
1028
+ }
1029
+ : undefined
992
1030
 
993
1031
  const responses: Array<ResponseNode> = operation.getResponseStatusCodes().map((statusCode) => {
994
1032
  const responseObj = operation.getResponseByStatusCode(statusCode)
@@ -1008,11 +1046,18 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
1008
1046
 
1009
1047
  const mediaType = rawContent ? toMediaType(Object.keys(rawContent)[0] ?? '') : toMediaType(operation.contentType ?? '')
1010
1048
 
1049
+ const keysToOmit = responseSchema?.properties
1050
+ ? Object.entries(responseSchema.properties)
1051
+ .filter(([, prop]) => !isReference(prop) && (prop as { writeOnly?: boolean }).writeOnly)
1052
+ .map(([key]) => key)
1053
+ : undefined
1054
+
1011
1055
  return createResponse({
1012
1056
  statusCode: statusCode as StatusCode,
1013
1057
  description,
1014
1058
  schema,
1015
1059
  mediaType,
1060
+ keysToOmit: keysToOmit?.length ? keysToOmit : undefined,
1016
1061
  })
1017
1062
  })
1018
1063
 
@@ -1034,8 +1079,8 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
1034
1079
  * Converts an OpenAPI/Swagger spec (wrapped in a Kubb `Oas` instance) into
1035
1080
  * a `RootNode` — the top-level node of the `@kubb/ast` tree.
1036
1081
  */
1037
- function parse<TOptions extends Partial<Options> = object>(options?: TOptions): RootNode {
1038
- const mergedOptions: Options = { ...DEFAULT_OPTIONS, ...options }
1082
+ function parse<TOptions extends Partial<ParserOptions> = object>(options?: TOptions): RootNode {
1083
+ const mergedOptions: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...options }
1039
1084
 
1040
1085
  const schemas: Array<SchemaNode> = Object.entries(schemaObjects).map(([name, schemaObject]) =>
1041
1086
  convertSchema({ schema: schemaObject as SchemaObject, name }, mergedOptions),
package/src/types.ts CHANGED
@@ -2,6 +2,32 @@ import type { AdapterFactoryOptions } from '@kubb/core'
2
2
  import type { Oas as OasClass } from './oas/Oas.ts'
3
3
  import type { contentType } from './oas/types.ts'
4
4
 
5
+ /**
6
+ * Controls how various OAS constructs are mapped to Kubb AST nodes.
7
+ */
8
+ export type ParserOptions = {
9
+ /**
10
+ * How `format: 'date-time'` schemas are represented. `false` falls through to a plain string.
11
+ */
12
+ dateType: false | 'string' | 'stringOffset' | 'stringLocal' | 'date'
13
+ /**
14
+ * Whether `type: 'integer'` and `format: 'int64'` produce `number` or `bigint` nodes.
15
+ */
16
+ integerType?: 'number' | 'bigint'
17
+ /**
18
+ * AST type used when no schema type can be inferred.
19
+ */
20
+ unknownType: 'any' | 'unknown' | 'void'
21
+ /**
22
+ * AST type used for completely empty schemas (`{}`).
23
+ */
24
+ emptySchemaType: 'any' | 'unknown' | 'void'
25
+ /**
26
+ * Suffix appended to derived enum names when building property schema names.
27
+ */
28
+ enumSuffix: 'enum' | (string & {})
29
+ }
30
+
5
31
  export type OasAdapterOptions = {
6
32
  /**
7
33
  * Validate the OpenAPI spec before parsing.
@@ -41,8 +67,13 @@ export type OasAdapterOptions = {
41
67
  */
42
68
  discriminator?: 'strict' | 'inherit'
43
69
  /**
44
- * Automatically resolve name collisions across schema components.
45
- * @default false
70
+ * Enable collision detection for inline enum and schema type names.
71
+ * When `true` (default), full-path names are used and name collisions
72
+ * across schema components are automatically resolved (e.g. `OrderParamsStatusEnum`).
73
+ * When `false`, enum names use only the immediate property context
74
+ * (e.g. `ParamsStatusEnum`) matching the v4 naming conventions, with numeric
75
+ * suffixes for deduplication (e.g. `ParamsStatusEnum2`).
76
+ * @default true
46
77
  */
47
78
  collisionDetection?: boolean
48
79
  /**
@@ -52,23 +83,7 @@ export type OasAdapterOptions = {
52
83
  * - `false` falls through to a plain `string` node.
53
84
  * @default 'string'
54
85
  */
55
- dateType?: false | 'string' | 'stringOffset' | 'stringLocal' | 'date'
56
- /**
57
- * Whether `type: 'integer'` / `format: 'int64'` produces `number` or `bigint` nodes.
58
- * @default 'number'
59
- */
60
- integerType?: 'number' | 'bigint'
61
- /**
62
- * AST type used when no schema type can be inferred.
63
- * @default 'any'
64
- */
65
- unknownType?: 'any' | 'unknown' | 'void'
66
- /**
67
- * AST type used for completely empty schemas (`{}`).
68
- * @default `unknownType`
69
- */
70
- emptySchemaType?: 'any' | 'unknown' | 'void'
71
- }
86
+ } & Partial<ParserOptions>
72
87
 
73
88
  export type OasAdapterResolvedOptions = {
74
89
  validate: boolean
@@ -82,6 +97,7 @@ export type OasAdapterResolvedOptions = {
82
97
  integerType: NonNullable<OasAdapterOptions['integerType']>
83
98
  unknownType: NonNullable<OasAdapterOptions['unknownType']>
84
99
  emptySchemaType: NonNullable<OasAdapterOptions['emptySchemaType']>
100
+ enumSuffix: OasAdapterOptions['enumSuffix']
85
101
  /**
86
102
  * Map from original `$ref` paths to their collision-resolved schema names.
87
103
  * Populated by the adapter after each `parse()` call.