@kubb/adapter-oas 5.0.0-alpha.2 → 5.0.0-alpha.20

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,209 +1,64 @@
1
- import { pascalCase } from '@internals/utils'
1
+ import { URLPath } from '@internals/utils'
2
2
  import {
3
- collect,
3
+ childName,
4
+ createDiscriminantNode,
4
5
  createOperation,
5
6
  createParameter,
6
7
  createProperty,
7
8
  createResponse,
8
9
  createRoot,
9
10
  createSchema,
11
+ enumPropName,
12
+ extractRefName,
13
+ findDiscriminator,
14
+ mergeAdjacentObjects,
10
15
  narrowSchema,
11
- schemaTypes,
12
- transform,
16
+ type ParserOptions,
17
+ setDiscriminatorEnum,
18
+ setEnumName,
19
+ simplifyUnion,
13
20
  } from '@kubb/ast'
14
21
  import type {
15
- ArraySchemaNode,
16
- DateSchemaNode,
17
- DatetimeSchemaNode,
18
- EnumSchemaNode,
22
+ DistributiveOmit,
19
23
  HttpMethod,
20
- IntersectionSchemaNode,
21
- MediaType,
22
- NumberSchemaNode,
23
- ObjectSchemaNode,
24
24
  OperationNode,
25
25
  ParameterLocation,
26
26
  ParameterNode,
27
27
  PrimitiveSchemaType,
28
28
  PropertyNode,
29
- RefSchemaNode,
30
29
  ResponseNode,
31
30
  RootNode,
32
- ScalarSchemaNode,
33
31
  ScalarSchemaType,
34
32
  SchemaNode,
35
- SchemaType,
36
33
  StatusCode,
37
- StringSchemaNode,
38
- TimeSchemaNode,
39
- UnionSchemaNode,
40
34
  } from '@kubb/ast/types'
41
- import type { KubbFile } from '@kubb/fabric-core/types'
42
- import { enumExtensionKeys, formatMap, knownMediaTypes } from './constants.ts'
43
- import type { Oas } from './oas/Oas.ts'
44
- import type { contentType, Operation, SchemaObject } from './oas/types.ts'
45
- import { flattenSchema, isDiscriminator, isNullable, isReference } from './oas/utils.ts'
46
-
47
- /**
48
- * Distributive `Omit` — correctly distributes over union types so that
49
- * `Omit<A | B, 'kind'>` produces `Omit<A, 'kind'> | Omit<B, 'kind'>`
50
- * rather than `Omit<A | B, 'kind'>`.
51
- */
52
- type DistributiveOmit<TValue, TKey extends PropertyKey> = TValue extends unknown ? Omit<TValue, TKey> : never
53
-
54
- /**
55
- * Maps each `dateType` option value to the AST node produced by `format: 'date-time'`.
56
- */
57
- type DateTimeNodeByDateType = {
58
- date: DateSchemaNode
59
- string: DatetimeSchemaNode
60
- stringOffset: DatetimeSchemaNode
61
- stringLocal: DatetimeSchemaNode
62
- false: StringSchemaNode
63
- }
64
-
65
- /**
66
- * Resolves the AST node produced by `format: 'date-time'` based on the `dateType` option.
67
- */
68
- type ResolveDateTimeNode<TDateType extends Options['dateType']> = DateTimeNodeByDateType[TDateType extends keyof DateTimeNodeByDateType ? TDateType : 'string']
69
-
70
- /**
71
- * Single source of truth: ordered list of `[shape, SchemaNode]` pairs.
72
- * `InferSchemaNode` walks this tuple in order and returns the node type of the first matching entry.
73
- * Parameterized over `TDateType` so `format: 'date-time'` resolves to the correct node based on the option.
74
- */
75
- type SchemaNodeMap<TDateType extends Options['dateType'] = Options['dateType']> = [
76
- [{ $ref: string }, RefSchemaNode],
77
- // allOf with sibling `properties` always produces an intersection (shared props are appended as a member).
78
- [{ allOf: ReadonlyArray<unknown>; properties: object }, IntersectionSchemaNode],
79
- // allOf with 2+ members always produces an intersection.
80
- [{ allOf: readonly [unknown, unknown, ...unknown[]] }, IntersectionSchemaNode],
81
- // Single-member allOf without sibling `properties` flattens to the member type.
82
- [{ allOf: ReadonlyArray<unknown> }, SchemaNode],
83
- [{ oneOf: ReadonlyArray<unknown> }, UnionSchemaNode],
84
- [{ anyOf: ReadonlyArray<unknown> }, UnionSchemaNode],
85
- [{ const: null }, ScalarSchemaNode],
86
- [{ const: string | number | boolean }, EnumSchemaNode],
87
- // OAS 3.1 multi-type array: `{ type: ['string', 'integer'] }` → union node.
88
- [{ type: ReadonlyArray<string> }, UnionSchemaNode],
89
- // `{ type: 'array', enum }` is normalized at runtime: enum moves into items → array node.
90
- [{ type: 'array'; enum: ReadonlyArray<unknown> }, ArraySchemaNode],
91
- [{ enum: ReadonlyArray<unknown> }, EnumSchemaNode],
92
- [{ type: 'object' }, ObjectSchemaNode],
93
- [{ additionalProperties: boolean | {} }, ObjectSchemaNode],
94
- [{ type: 'array' }, ArraySchemaNode],
95
- [{ items: object }, ArraySchemaNode],
96
- [{ prefixItems: ReadonlyArray<unknown> }, ArraySchemaNode],
97
- // Format entries with explicit type — placed before generic type entries so format wins.
98
- [{ type: string; format: 'date-time' }, ResolveDateTimeNode<TDateType>],
99
- [{ type: string; format: 'date' }, DateSchemaNode],
100
- [{ type: string; format: 'time' }, TimeSchemaNode],
101
- [{ format: 'date-time' }, ResolveDateTimeNode<TDateType>],
102
- [{ format: 'date' }, DateSchemaNode],
103
- [{ format: 'time' }, TimeSchemaNode],
104
- [{ type: 'string' }, StringSchemaNode],
105
- [{ type: 'number' }, NumberSchemaNode],
106
- [{ type: 'integer' }, NumberSchemaNode],
107
- [{ type: 'bigint' }, NumberSchemaNode],
108
- [{ type: string }, ScalarSchemaNode],
109
- // Inferred scalar types from constraints when no explicit type is present.
110
- [{ minLength: number }, StringSchemaNode],
111
- [{ maxLength: number }, StringSchemaNode],
112
- [{ pattern: string }, StringSchemaNode],
113
- [{ minimum: number }, NumberSchemaNode],
114
- [{ maximum: number }, NumberSchemaNode],
115
- ]
116
-
117
- export type InferSchemaNode<
118
- TSchema extends SchemaObject,
119
- TDateType extends Options['dateType'] = Options['dateType'],
120
- TEntries extends ReadonlyArray<[object, SchemaNode]> = SchemaNodeMap<TDateType>,
121
- > = TEntries extends [infer TEntry extends [object, SchemaNode], ...infer TRest extends ReadonlyArray<[object, SchemaNode]>]
122
- ? TSchema extends TEntry[0]
123
- ? TEntry[1]
124
- : InferSchemaNode<TSchema, TDateType, TRest>
125
- : SchemaNode
126
-
127
- /**
128
- * Controls how various OAS constructs are mapped to Kubb AST nodes.
129
- */
130
- export type Options = {
131
- /**
132
- * How `format: 'date-time'` schemas are represented. `false` falls through to a plain string.
133
- */
134
- dateType: false | 'string' | 'stringOffset' | 'stringLocal' | 'date'
135
- /**
136
- * Whether `type: 'integer'` and `format: 'int64'` produce `number` or `bigint` nodes.
137
- */
138
- integerType?: 'number' | 'bigint'
139
- /**
140
- * AST type used when no schema type can be inferred.
141
- */
142
- unknownType: 'any' | 'unknown' | 'void'
143
- /**
144
- * AST type used for completely empty schemas (`{}`).
145
- */
146
- emptySchemaType: 'any' | 'unknown' | 'void'
147
- /**
148
- * Suffix appended to derived enum names when building property schema names.
149
- */
150
- enumSuffix: string
151
- }
35
+ import BaseOas from 'oas'
36
+ import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, typeOptionMap } from './constants.ts'
37
+ import { isDiscriminator, isNullable, isReference } from './guards.ts'
38
+ import { resolveRef } from './refs.ts'
39
+ import {
40
+ buildSchemaNode,
41
+ flattenSchema,
42
+ getDateType,
43
+ getMediaType,
44
+ getParameters,
45
+ getPrimitiveType,
46
+ getRequestSchema,
47
+ getResponseSchema,
48
+ getSchemas,
49
+ getSchemaType,
50
+ } from './resolvers.ts'
51
+ import type { contentType, Document, Operation, ReferenceObject, SchemaObject } from './types.ts'
152
52
 
153
53
  /**
154
- * Construction-time options for `createOasParser`.
54
+ * Construction-time context for the OAS parser.
55
+ *
56
+ * Holds the raw OpenAPI document and optional content-type override used when extracting
57
+ * request/response schemas.
155
58
  */
156
- export type OasParserOptions = {
59
+ export type OasParserContext = {
60
+ document: Document
157
61
  contentType?: contentType
158
- collisionDetection?: boolean
159
- }
160
-
161
- /**
162
- * Default values for all `Options` fields.
163
- */
164
- const DEFAULT_OPTIONS = {
165
- dateType: 'string',
166
- integerType: 'number',
167
- unknownType: 'any',
168
- emptySchemaType: 'any',
169
- enumSuffix: 'enum',
170
- } as const satisfies Options
171
-
172
- /**
173
- * Looks up the Kubb `SchemaType` for a given OAS `format` string.
174
- * Returns `undefined` for formats not in `formatMap` (e.g. `int64`, `date-time`),
175
- * which are handled separately because their output depends on parser options.
176
- */
177
- function formatToSchemaType(format: string): SchemaType | undefined {
178
- return formatMap[format as keyof typeof formatMap]
179
- }
180
-
181
- /**
182
- * Extracts the final path segment of a JSON Pointer `$ref` string.
183
- * For `#/components/schemas/Order` this returns `'Order'`.
184
- * Falls back to the full ref string when no slash is present.
185
- */
186
- function extractRefName($ref: string): string {
187
- return $ref.split('/').at(-1) ?? $ref
188
- }
189
-
190
- /**
191
- * Maps an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
192
- * Numeric types (`number`, `integer`, `bigint`) are returned unchanged;
193
- * `boolean` maps to `'boolean'`; everything else defaults to `'string'`.
194
- */
195
- function getPrimitiveType(type: string | undefined): PrimitiveSchemaType {
196
- if (type === 'number' || type === 'integer' || type === 'bigint') return type
197
- if (type === 'boolean') return 'boolean'
198
- return 'string'
199
- }
200
-
201
- /**
202
- * Narrows a raw content-type string to the `MediaType` union recognized by Kubb.
203
- * Returns `undefined` for content types not present in `KNOWN_MEDIA_TYPES`.
204
- */
205
- function toMediaType(contentType: string): MediaType | undefined {
206
- return knownMediaTypes.includes(contentType as MediaType) ? (contentType as MediaType) : undefined
207
62
  }
208
63
 
209
64
  /**
@@ -212,217 +67,72 @@ function toMediaType(contentType: string): MediaType | undefined {
212
67
  */
213
68
  type SchemaContext = {
214
69
  schema: SchemaObject
215
- name: string | undefined
70
+ name: string | null | undefined
216
71
  nullable: true | undefined
217
72
  defaultValue: unknown
218
73
  /**
219
74
  * Normalized single type string (first element when OAS 3.1 multi-type array).
220
75
  */
221
76
  type: string | undefined
222
- options: Partial<Options> | undefined
223
- mergedOptions: Options
77
+ rawOptions: Partial<ParserOptions> | undefined
78
+ options: ParserOptions
224
79
  }
225
80
 
226
81
  /**
227
- * The public interface returned by `createOasParser`.
82
+ * Normalize a malformed `{ type: 'array', enum: [...] }` schema by moving the
83
+ * enum values into the items sub-schema. This pattern is technically invalid OAS
84
+ * but appears in the wild and must be handled gracefully.
228
85
  */
229
- export type OasParser = {
230
- /**
231
- * Converts an OpenAPI/Swagger spec (wrapped in a Kubb `Oas` instance) into
232
- * a `RootNode` the top-level node of the `@kubb/ast` tree.
233
- */
234
- parse: <TOptions extends Partial<Options> = object>(options?: TOptions) => RootNode
235
- convertSchema: <TFormat extends string, TSchema extends SchemaObject & { format?: TFormat }, TOptions extends Partial<Options> = object>(
236
- params: { schema: TSchema; name?: string },
237
- options?: TOptions,
238
- ) => InferSchemaNode<TSchema, TOptions extends { dateType: Options['dateType'] } ? TOptions['dateType'] : (typeof DEFAULT_OPTIONS)['dateType']>
239
- /**
240
- * Walks `node` and replaces each `ref` value with the name returned by
241
- * `resolveName`. The callback receives the full `$ref` path (e.g. `#/components/schemas/Order`)
242
- * when available, falling back to the short name. Pass a no-op (`(n) => n`) to skip resolution.
243
- *
244
- * The optional `resolveEnumName` callback is called for inline `enum` nodes and should return
245
- * the transformed name to use (e.g. with a plugin `transformers.name` applied).
246
- */
247
- resolveRefs: (node: SchemaNode, resolveName: (ref: string) => string | undefined, resolveEnumName?: (name: string) => string | undefined) => SchemaNode
248
- /**
249
- * Extracts `KubbFile.Import` entries from a `SchemaNode` tree by collecting
250
- * all importable `ref` nodes. A `$ref` is considered importable when it resolves
251
- * to a known component in the OAS spec (`oas.get($ref)` is truthy).
252
- *
253
- * The `resolve` callback is called with the schema name (last segment of the
254
- * `$ref`, collision-corrected via the OAS name mapping) and must return the
255
- * `{ name, path }` pair for the generated import, or `undefined` to skip it.
256
- *
257
- * @example
258
- * ```ts
259
- * const imports = parser.getImports(schemaNode, (schemaName) => ({
260
- * name: schemaManager.getName(schemaName, { type: 'type' }),
261
- * path: schemaManager.getFile(schemaName).path,
262
- * }))
263
- * ```
264
- */
265
- getImports: (node: SchemaNode, resolve: (schemaName: string) => { name: string; path: string } | undefined) => Array<KubbFile.Import>
266
- }
86
+ function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
87
+ const isItemsObject = typeof schema.items === 'object' && !Array.isArray(schema.items)
88
+ const normalizedItems: SchemaObject = { ...(isItemsObject ? (schema.items as SchemaObject) : {}), enum: schema.enum }
89
+ const { enum: _enum, ...schemaWithoutEnum } = schema
267
90
 
268
- /**
269
- * When a discriminator is present, replaces the discriminator property's schema with
270
- * an enum of the mapping keys so downstream code emits a precise literal-union type.
271
- * Returns the original schema unchanged when there is no discriminator or no mapping.
272
- */
273
- function applyDiscriminatorEnum(schema: SchemaObject): SchemaObject {
274
- if (!isDiscriminator(schema)) return schema
275
- const propName = schema.discriminator.propertyName
276
- if (!schema.properties?.[propName]) return schema
277
- return {
278
- ...schema,
279
- properties: {
280
- ...schema.properties,
281
- [propName]: {
282
- ...(schema.properties[propName] as SchemaObject),
283
- enum: schema.discriminator.mapping ? Object.keys(schema.discriminator.mapping) : undefined,
284
- },
285
- },
286
- } as SchemaObject
91
+ return { ...schemaWithoutEnum, items: normalizedItems } as SchemaObject
287
92
  }
288
93
 
289
94
  /**
290
- * Creates an OAS parser that converts an OpenAPI/Swagger spec into
291
- * the `@kubb/ast` tree.
292
- *
293
- * Options are passed per-call to `parse` or `convertSchema` rather than
294
- * at construction time, keeping the factory lightweight.
295
- *
296
- * This is the **kubb-parser** stage of the compilation lifecycle:
297
- * OpenAPI / Swagger → Kubb AST
95
+ * Builds the internal converter functions for a given `OasParserContext`.
298
96
  *
299
- * No code is generated here; the resulting tree is spec-agnostic and can
300
- * be consumed by any downstream plugin (plugin-ts, plugin-zod, …).
301
- *
302
- * @example
303
- * ```ts
304
- * const parser = createOasParser(oas)
305
- * const root = parser.parse({ emptySchemaType: 'unknown' })
306
- * ```
97
+ * All `convert*` functions are defined as function declarations so they can freely
98
+ * reference each other and `parseSchema` via JS hoisting (mutual recursion).
307
99
  */
308
- export function createOasParser(oas: Oas, { contentType, collisionDetection }: OasParserOptions = {}): OasParser {
309
- // Map from original component paths to resolved schema names (after collision resolution)
310
- // e.g., { '#/components/schemas/Order': 'OrderSchema', '#/components/responses/Product': 'ProductResponse' }
311
- const { schemas: schemaObjects, nameMapping } = oas.getSchemas({ contentType, collisionDetection })
312
-
313
- /**
314
- * Maps an `'any' | 'unknown' | 'void'` option string to the corresponding `SchemaType` constant.
315
- * Used for both `unknownType` (unannotated schemas) and `emptySchemaType` (empty `{}` schemas).
316
- */
317
- function resolveTypeOption(value: 'any' | 'unknown' | 'void'): ScalarSchemaType {
318
- if (value === 'any') return schemaTypes.any
319
- if (value === 'void') return schemaTypes.void
320
- return schemaTypes.unknown
321
- }
322
-
323
- /**
324
- * Resolves the AST type and datetime modifiers for a date/time format, honoring the `dateType` option.
325
- * Returns `undefined` when `dateType` is `false`, meaning the format should fall through to `string`.
326
- */
327
- function getDateType(
328
- options: Options,
329
- format: 'date-time' | 'date' | 'time',
330
- ): { type: 'datetime'; offset?: boolean; local?: boolean } | { type: 'date' | 'time'; representation: 'date' | 'string' } | undefined {
331
- if (!options.dateType) {
332
- return undefined
333
- }
334
-
335
- if (format === 'date-time') {
336
- if (options.dateType === 'date') {
337
- return { type: 'date', representation: 'date' }
338
- }
339
- if (options.dateType === 'stringOffset') {
340
- return { type: 'datetime', offset: true }
341
- }
342
- if (options.dateType === 'stringLocal') {
343
- return { type: 'datetime', local: true }
344
- }
345
- return { type: 'datetime', offset: false }
346
- }
347
-
348
- if (format === 'date') {
349
- return { type: 'date', representation: options.dateType === 'date' ? 'date' : 'string' }
350
- }
100
+ function createSchemaParser(ctx: OasParserContext) {
101
+ const document = ctx.document
351
102
 
352
- // time
353
- return { type: 'time', representation: options.dateType === 'date' ? 'date' : 'string' }
354
- }
103
+ // Branch handlers — each converts one OAS schema pattern to a SchemaNode.
355
104
 
356
105
  /**
357
- * Shared metadata fields included in every `createSchema` call.
358
- * Centralizes the common properties so sub-handlers don't repeat them.
106
+ * Converts a `$ref` schema into a `RefSchemaNode`.
359
107
  */
360
- function buildSchemaBase(schema: SchemaObject, name: string | undefined, nullable: true | undefined, defaultValue: unknown) {
361
- return {
362
- name,
108
+ function convertRef({ schema, nullable, defaultValue }: SchemaContext): SchemaNode {
109
+ return createSchema({
110
+ type: 'ref',
111
+ name: extractRefName(schema.$ref!),
112
+ ref: schema.$ref,
363
113
  nullable,
364
- title: schema.title,
365
114
  description: schema.description,
366
115
  deprecated: schema.deprecated,
367
116
  readOnly: schema.readOnly,
368
117
  writeOnly: schema.writeOnly,
369
- default: defaultValue,
118
+ pattern: schema.type === 'string' ? schema.pattern : undefined,
370
119
  example: schema.example,
371
- } as const
372
- }
373
-
374
- // Branch handlers — each converts one OAS schema pattern to a SchemaNode.
375
- // They are defined as function declarations so they can reference each other
376
- // and `convertSchema` freely (JS hoisting).
377
-
378
- /**
379
- * Converts a `$ref` schema pointer into a `RefSchemaNode`.
380
- *
381
- * In OAS 3.0 siblings of `$ref` are technically ignored by the spec, but Kubb intentionally
382
- * preserves them so that annotations like `pattern`, `description`, and `nullable` are
383
- * reflected in generated JSDoc and type modifiers.
384
- */
385
- function convertRef({ schema, nullable, defaultValue }: SchemaContext): SchemaNode {
386
- const schemaObject = schema as unknown as SchemaObject & { $ref: string }
387
- return createSchema({
388
- type: 'ref',
389
- name: extractRefName(schemaObject.$ref),
390
- ref: schemaObject.$ref,
391
- nullable,
392
- description: schemaObject.description,
393
- deprecated: schemaObject.deprecated,
394
- readOnly: schemaObject.readOnly,
395
- writeOnly: schemaObject.writeOnly,
396
- pattern: schemaObject.type === 'string' ? schemaObject.pattern : undefined,
397
- example: schemaObject.example,
398
120
  default: defaultValue,
399
121
  })
400
122
  }
401
123
 
402
124
  /**
403
- * Converts a `allOf` schema into either a flattened member node (single-member `allOf`)
404
- * or an `IntersectionSchemaNode` (multi-member `allOf`).
405
- *
406
- * Single-member `allOf` without sibling structural keys is the common OAS 3.0 pattern for
407
- * annotating a `$ref` or primitive with extra constraints; it is flattened to avoid
408
- * producing needless intersection wrappers.
409
- *
410
- * The flatten path is skipped when the outer schema carries structural keys that cannot be
411
- * merged into annotation fields: `properties`, `required`, or `additionalProperties`.
412
- * Those cases must become an intersection so the constraints are preserved.
413
- *
414
- * Circular references through discriminator parents are detected and skipped to prevent
415
- * infinite recursion during code generation.
125
+ * Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
416
126
  */
417
- function convertAllOf({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode {
127
+ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
418
128
  if (
419
129
  schema.allOf!.length === 1 &&
420
130
  !schema.properties &&
421
131
  !(Array.isArray(schema.required) && schema.required.length) &&
422
132
  schema.additionalProperties === undefined
423
133
  ) {
424
- const [memberSchema] = schema.allOf as SchemaObject[]
425
- const memberNode = convertSchema({ schema: memberSchema! }, options)
134
+ const [memberSchema] = schema.allOf as Array<SchemaObject | ReferenceObject>
135
+ const memberNode = parseSchema({ schema: memberSchema! as SchemaObject, name: null }, rawOptions)
426
136
  const { kind: _kind, ...memberNodeProps } = memberNode
427
137
  const mergedNullable = nullable || memberNode.nullable || undefined
428
138
  const mergedDefault = schema.default === null && mergedNullable ? undefined : (schema.default ?? memberNode.default)
@@ -442,41 +152,45 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
442
152
  } as DistributiveOmit<SchemaNode, 'kind'>)
443
153
  }
444
154
 
445
- // When a child schema extends a discriminator parent via allOf and the parent's oneOf/anyOf
446
- // references that child back, skip that allOf item to prevent a circular type reference.
447
- const allOfMembers: SchemaNode[] = (schema.allOf as SchemaObject[])
155
+ const filteredDiscriminantValues: Array<{ propertyName: string; value: string }> = []
156
+ const allOfMembers: Array<SchemaNode> = (schema.allOf as Array<SchemaObject | ReferenceObject>)
448
157
  .filter((item) => {
449
158
  if (!isReference(item) || !name) return true
450
- const deref = oas.get<SchemaObject>((item as { $ref: string }).$ref)
159
+ const deref = resolveRef<SchemaObject>(document, item.$ref)
451
160
  if (!deref || !isDiscriminator(deref)) return true
452
- const parentUnion = (deref as SchemaObject).oneOf ?? (deref as SchemaObject).anyOf
161
+ const parentUnion = deref.oneOf ?? deref.anyOf
453
162
  if (!parentUnion) return true
454
163
  const childRef = `#/components/schemas/${name}`
455
- const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && (oneOfItem as { $ref: string }).$ref === childRef)
456
- const inMapping = Object.values((deref as SchemaObject & { discriminator: { mapping?: Record<string, string> } }).discriminator.mapping ?? {}).some(
457
- (v) => v === childRef,
458
- )
459
- return !inOneOf && !inMapping
164
+ const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef)
165
+ const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef)
166
+ if (inOneOf || inMapping) {
167
+ const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef)
168
+ if (discriminatorValue) {
169
+ filteredDiscriminantValues.push({ propertyName: deref.discriminator.propertyName, value: discriminatorValue })
170
+ }
171
+ return false
172
+ }
173
+ return true
460
174
  })
461
- .map((s) => convertSchema({ schema: s as SchemaObject }, options))
175
+ .map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))
176
+
177
+ const syntheticStart = allOfMembers.length
462
178
 
463
- // When `required` lists keys not present in the outer `properties`, resolve them from
464
- // the allOf member schemas and inject them as extra intersection members.
465
179
  if (Array.isArray(schema.required) && schema.required.length) {
466
180
  const outerKeys = schema.properties ? new Set(Object.keys(schema.properties)) : new Set<string>()
467
181
  const missingRequired = schema.required.filter((key) => !outerKeys.has(key))
468
182
 
469
183
  if (missingRequired.length) {
470
- const resolvedMembers = (schema.allOf as SchemaObject[]).flatMap((item) => {
471
- if (!isReference(item)) return [item]
472
- const deref = oas.get<SchemaObject>(item.$ref)
473
- return deref && !isReference(deref) ? [deref as SchemaObject] : []
184
+ const resolvedMembers = (schema.allOf as Array<SchemaObject | ReferenceObject>).flatMap((item) => {
185
+ if (!isReference(item)) return [item as SchemaObject]
186
+ const deref = resolveRef<SchemaObject>(document, item.$ref)
187
+ return deref && !isReference(deref) ? [deref] : []
474
188
  })
475
189
 
476
190
  for (const key of missingRequired) {
477
191
  for (const resolved of resolvedMembers) {
478
192
  if (resolved.properties?.[key]) {
479
- allOfMembers.push(convertSchema({ schema: { properties: { [key]: resolved.properties[key] }, required: [key] } as SchemaObject }, options))
193
+ allOfMembers.push(parseSchema({ schema: { properties: { [key]: resolved.properties[key] }, required: [key] } as SchemaObject }, rawOptions))
480
194
  break
481
195
  }
482
196
  }
@@ -485,59 +199,109 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
485
199
  }
486
200
 
487
201
  if (schema.properties) {
488
- const { allOf: _allOf, ...schemaWithoutAllOf } = schema as SchemaObject & { allOf?: unknown[] }
489
- allOfMembers.push(convertSchema({ schema: schemaWithoutAllOf as SchemaObject }, options))
202
+ const { allOf: _allOf, ...schemaWithoutAllOf } = schema
203
+ allOfMembers.push(parseSchema({ schema: schemaWithoutAllOf }, rawOptions))
204
+ }
205
+
206
+ for (const { propertyName, value } of filteredDiscriminantValues) {
207
+ allOfMembers.push(createDiscriminantNode({ propertyName, value }))
490
208
  }
491
209
 
492
210
  return createSchema({
493
211
  type: 'intersection',
494
- members: allOfMembers,
495
- ...buildSchemaBase(schema, name, nullable, defaultValue),
212
+ members: [...mergeAdjacentObjects(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentObjects(allOfMembers.slice(syntheticStart))],
213
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
496
214
  })
497
215
  }
498
216
 
499
217
  /**
500
218
  * Converts a `oneOf` / `anyOf` schema into a `UnionSchemaNode`.
501
- *
502
- * Both keywords are treated identically — their members are concatenated into a single union.
503
- * When sibling `properties` are present alongside `oneOf`/`anyOf`, each union member is
504
- * individually intersected with the shared properties node to match the OAS pattern of
505
- * adding common fields next to a discriminated union.
506
219
  */
507
- function convertUnion({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode {
220
+ function convertUnion({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
221
+ function pickDiscriminatorPropertyNode(node: SchemaNode, propertyName: string): SchemaNode | null {
222
+ const objectNode = narrowSchema(node, 'object')
223
+ const discriminatorProperty = objectNode?.properties?.find((property) => property.name === propertyName)
224
+
225
+ if (!discriminatorProperty) {
226
+ return null
227
+ }
228
+
229
+ return createSchema({
230
+ type: 'object',
231
+ primitive: 'object',
232
+ properties: [discriminatorProperty],
233
+ })
234
+ }
235
+
508
236
  const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
509
237
  const unionBase = {
510
- ...buildSchemaBase(schema, name, nullable, defaultValue),
238
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
511
239
  discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
512
240
  }
241
+ const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
242
+ const sharedPropertiesNode = schema.properties
243
+ ? (() => {
244
+ const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
245
+ const memberBaseSchema: SchemaObject = discriminator
246
+ ? (Object.fromEntries(Object.entries(schemaWithoutUnion).filter(([key]) => key !== 'discriminator')) as SchemaObject)
247
+ : schemaWithoutUnion
248
+ return parseSchema({ schema: memberBaseSchema, name }, rawOptions)
249
+ })()
250
+ : undefined
513
251
 
514
- if (schema.properties) {
515
- const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema as SchemaObject & { oneOf?: unknown[]; anyOf?: unknown[] }
516
- const propertiesNode = convertSchema({ schema: schemaWithoutUnion as SchemaObject }, options)
252
+ if (sharedPropertiesNode || discriminator?.mapping) {
253
+ const members = unionMembers.map((s) => {
254
+ const ref = isReference(s) ? s.$ref : undefined
255
+ const discriminatorValue = findDiscriminator(discriminator?.mapping, ref)
256
+ const memberNode = parseSchema({ schema: s as SchemaObject }, rawOptions)
517
257
 
518
- return createSchema({
258
+ if (!discriminatorValue || !discriminator) {
259
+ return memberNode
260
+ }
261
+
262
+ const narrowedDiscriminatorNode = sharedPropertiesNode
263
+ ? pickDiscriminatorPropertyNode(
264
+ setDiscriminatorEnum({
265
+ node: sharedPropertiesNode,
266
+ propertyName: discriminator.propertyName,
267
+ values: [discriminatorValue],
268
+ }),
269
+ discriminator.propertyName,
270
+ )
271
+ : undefined
272
+
273
+ return createSchema({
274
+ type: 'intersection',
275
+ members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({ propertyName: discriminator.propertyName, value: discriminatorValue })],
276
+ })
277
+ })
278
+
279
+ const unionNode = createSchema({
519
280
  type: 'union',
520
281
  ...unionBase,
521
- members: unionMembers.map((s) =>
522
- createSchema({
523
- type: 'intersection',
524
- members: [convertSchema({ schema: s as SchemaObject }, options), propertiesNode],
525
- }),
526
- ),
282
+ members,
283
+ })
284
+
285
+ if (!sharedPropertiesNode) {
286
+ return unionNode
287
+ }
288
+
289
+ return createSchema({
290
+ type: 'intersection',
291
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
292
+ members: [unionNode, sharedPropertiesNode],
527
293
  })
528
294
  }
529
295
 
530
296
  return createSchema({
531
297
  type: 'union',
532
298
  ...unionBase,
533
- members: unionMembers.map((s) => convertSchema({ schema: s as SchemaObject }, options)),
299
+ members: simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))),
534
300
  })
535
301
  }
536
302
 
537
303
  /**
538
- * Converts an OAS 3.1 `const` schema into either a null scalar or a single-value `EnumSchemaNode`.
539
- * `const: null` maps to a null scalar; any other value becomes a one-item enum so that generators
540
- * can produce a precise literal type.
304
+ * Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
541
305
  */
542
306
  function convertConst({ schema, name, nullable, defaultValue }: SchemaContext): SchemaNode {
543
307
  const constValue = schema.const
@@ -550,7 +314,6 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
550
314
  title: schema.title,
551
315
  description: schema.description,
552
316
  deprecated: schema.deprecated,
553
- nullable,
554
317
  })
555
318
  }
556
319
 
@@ -559,22 +322,20 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
559
322
  type: 'enum',
560
323
  primitive: constPrimitive,
561
324
  enumValues: [constValue as string | number | boolean],
562
- ...buildSchemaBase(schema, name, nullable, defaultValue),
325
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
563
326
  })
564
327
  }
565
328
 
566
329
  /**
567
- * Handles `format`-based special types (date/time, uuid, email, blob, etc.).
568
- * Returns `undefined` when the format should fall through to string handling
569
- * (i.e. `format: 'date-time'` with `dateType: false`).
330
+ * Converts a format-annotated schema into a special-type `SchemaNode`.
331
+ * Returns `null` when the format should fall through to string handling (`dateType: false`).
570
332
  */
571
- function convertFormat({ schema, name, nullable, defaultValue, mergedOptions }: SchemaContext): SchemaNode | undefined {
572
- const base = buildSchemaBase(schema, name, nullable, defaultValue)
333
+ function convertFormat({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode | null {
334
+ const base = buildSchemaNode(schema, name, nullable, defaultValue)
573
335
 
574
- // int64 is option-dependent so it can't live in the static formatMap.
575
336
  if (schema.format === 'int64') {
576
337
  return createSchema({
577
- type: mergedOptions.integerType === 'bigint' ? 'bigint' : 'integer',
338
+ type: options.integerType === 'bigint' ? 'bigint' : 'integer',
578
339
  primitive: 'integer',
579
340
  ...base,
580
341
  min: schema.minimum,
@@ -584,10 +345,9 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
584
345
  })
585
346
  }
586
347
 
587
- // date-time / date / time are option-dependent and can't live in the static formatMap.
588
348
  if (schema.format === 'date-time' || schema.format === 'date' || schema.format === 'time') {
589
- const dateType = getDateType(mergedOptions, schema.format)
590
- if (!dateType) return undefined // dateType: false → fall through to string
349
+ const dateType = getDateType(options, schema.format)
350
+ if (!dateType) return null
591
351
 
592
352
  if (dateType.type === 'datetime') {
593
353
  return createSchema({ ...base, primitive: 'string' as const, type: 'datetime', offset: dateType.offset, local: dateType.local })
@@ -595,38 +355,29 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
595
355
  return createSchema({ ...base, primitive: 'string' as const, type: dateType.type, representation: dateType.representation })
596
356
  }
597
357
 
598
- const specialType = formatToSchemaType(schema.format!)
599
- if (!specialType) return undefined
358
+ const specialType = getSchemaType(schema.format!)
359
+ if (!specialType) return null
600
360
 
601
361
  const specialPrimitive: PrimitiveSchemaType = specialType === 'number' || specialType === 'integer' || specialType === 'bigint' ? specialType : 'string'
602
362
 
603
363
  if (specialType === 'number' || specialType === 'integer' || specialType === 'bigint') {
604
364
  return createSchema({ ...base, primitive: specialPrimitive, type: specialType })
605
365
  }
366
+ if (specialType === 'url') {
367
+ return createSchema({ ...base, primitive: 'string' as const, type: 'url' })
368
+ }
369
+
606
370
  return createSchema({ ...base, primitive: specialPrimitive, type: specialType as ScalarSchemaType })
607
371
  }
608
372
 
609
373
  /**
610
374
  * Converts an `enum` schema into an `EnumSchemaNode`.
611
- *
612
- * Handles several edge cases:
613
- * - `{ type: 'array', enum }` (technically invalid OAS) — the enum is normalized into `items`.
614
- * - `null` in enum values (OAS 3.0 nullable enum convention) — stripped and reflected as `nullable`.
615
- * - `x-enumNames` / `x-enum-varnames` vendor extensions — produce named enum variants with explicit labels.
616
- * - Numeric and boolean enums require a const-map representation because most generators cannot
617
- * use string-enum syntax for non-string values.
618
375
  */
619
- function convertEnum({ schema, name, nullable, type, options }: SchemaContext): SchemaNode {
620
- // Malformed schema: `{ type: 'array', enum: [...] }` — normalize by moving the enum into items.
376
+ function convertEnum({ schema, name, nullable, type, rawOptions }: SchemaContext): SchemaNode {
621
377
  if (type === 'array') {
622
- const rawSchema = schema as unknown as { items?: SchemaObject; enum?: unknown[] }
623
- const isItemsObject = typeof rawSchema.items === 'object' && !Array.isArray(rawSchema.items)
624
- const normalizedItems = { ...(isItemsObject ? rawSchema.items : {}), enum: schema.enum } as SchemaObject
625
- const { enum: _enum, ...schemaWithoutEnum } = schema as SchemaObject & { enum?: unknown[] }
626
- return convertSchema({ schema: { ...schemaWithoutEnum, items: normalizedItems } as SchemaObject, name }, options)
378
+ return parseSchema({ schema: normalizeArrayEnum(schema), name }, rawOptions)
627
379
  }
628
380
 
629
- // `null` in enum values is the OAS 3.0 convention for a nullable enum.
630
381
  const nullInEnum = schema.enum!.includes(null)
631
382
  const filteredValues = (nullInEnum ? schema.enum!.filter((v) => v !== null) : schema.enum!) as Array<string | number | boolean>
632
383
  const enumNullable = nullable || nullInEnum || undefined
@@ -647,56 +398,27 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
647
398
  example: schema.example,
648
399
  }
649
400
 
650
- // x-enumNames / x-enum-varnames: named variants with explicit labels take priority.
651
401
  const extensionKey = enumExtensionKeys.find((key) => key in schema)
652
- if (extensionKey) {
653
- const rawNames = (schema as Record<string, unknown>)[extensionKey] as Array<string | number>
654
- const uniqueNames = [...new Set(rawNames)]
655
- const enumType =
656
- getPrimitiveType(type) === 'number' || getPrimitiveType(type) === 'integer'
657
- ? ('number' as const)
658
- : getPrimitiveType(type) === 'boolean'
659
- ? ('boolean' as const)
660
- : ('string' as const)
402
+ if (extensionKey || enumPrimitive === 'number' || enumPrimitive === 'integer' || enumPrimitive === 'boolean') {
403
+ const enumPrimitiveType = (enumPrimitive === 'number' || enumPrimitive === 'integer' ? 'number' : enumPrimitive === 'boolean' ? 'boolean' : 'string') as
404
+ | 'number'
405
+ | 'boolean'
406
+ | 'string'
407
+ const sourceValues = extensionKey
408
+ ? [...new Set((schema as Record<string, unknown>)[extensionKey] as Array<string | number>)]
409
+ : [...new Set(filteredValues)]
661
410
 
662
411
  return createSchema({
663
412
  ...enumBase,
664
- enumType,
665
- namedEnumValues: uniqueNames.map((label, index) => ({
413
+ primitive: enumPrimitiveType,
414
+ namedEnumValues: sourceValues.map((label, index) => ({
666
415
  name: String(label),
667
- value: filteredValues[index] ?? label,
668
- format: enumType,
669
- })),
670
- })
671
- }
672
-
673
- // Number / integer enum — must use a const map since most generators can't use string-enum for numbers.
674
- if (type === 'number' || type === 'integer') {
675
- return createSchema({
676
- ...enumBase,
677
- enumType: 'number' as const,
678
- namedEnumValues: [...new Set(filteredValues)].map((value) => ({
679
- name: String(value),
680
- value: value as number,
681
- format: 'number' as const,
682
- })),
683
- })
684
- }
685
-
686
- // Boolean enum — same const-map approach as numeric.
687
- if (type === 'boolean') {
688
- return createSchema({
689
- ...enumBase,
690
- enumType: 'boolean' as const,
691
- namedEnumValues: [...new Set(filteredValues)].map((value) => ({
692
- name: String(value),
693
- value: value as boolean,
694
- format: 'boolean' as const,
416
+ value: extensionKey ? (filteredValues[index] ?? label) : label,
417
+ primitive: enumPrimitiveType,
695
418
  })),
696
419
  })
697
420
  }
698
421
 
699
- // Plain string enum (default path).
700
422
  return createSchema({
701
423
  ...enumBase,
702
424
  enumValues: [...new Set(filteredValues)],
@@ -704,88 +426,88 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
704
426
  }
705
427
 
706
428
  /**
707
- * Converts an object-like schema (`type: 'object'`, `properties`, `additionalProperties`,
708
- * or `patternProperties`) into an `ObjectSchemaNode`.
709
- *
710
- * When a `discriminator` is present, the discriminator property's schema is replaced with an
711
- * enum of the mapping keys so generators can produce a precise literal-union type for it.
712
- *
713
- * Property optionality follows OAS semantics:
714
- * - required + not nullable → `required: true`
715
- * - not required + not nullable → `optional: true`
716
- * - not required + nullable → `nullish: true`
429
+ * Converts an object-like schema into an `ObjectSchemaNode`.
717
430
  */
718
- function convertObject({ schema, name, nullable, defaultValue, options, mergedOptions }: SchemaContext): SchemaNode {
719
- // When a discriminator is present, override the discriminator property's schema to use
720
- // an enum of the mapping keys for a precise literal-union type.
721
- const resolvedSchema = applyDiscriminatorEnum(schema)
722
-
723
- const properties: Array<PropertyNode> = resolvedSchema.properties
724
- ? Object.entries(resolvedSchema.properties).map(([propName, propSchema]) => {
725
- const required = Array.isArray(resolvedSchema.required) ? resolvedSchema.required.includes(propName) : !!resolvedSchema.required
431
+ function convertObject({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): SchemaNode {
432
+ const properties: Array<PropertyNode> = schema.properties
433
+ ? Object.entries(schema.properties).map(([propName, propSchema]) => {
434
+ const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required
726
435
  const resolvedPropSchema = propSchema as SchemaObject
727
436
  const propNullable = isNullable(resolvedPropSchema)
728
- const derivedPropName = name ? pascalCase([name, propName, mergedOptions.enumSuffix].filter(Boolean).join(' ')) : undefined
437
+
438
+ const resolvedChildName = childName(name, propName)
439
+ const propNode = parseSchema({ schema: resolvedPropSchema, name: resolvedChildName }, rawOptions)
440
+ let schemaNode = setEnumName(propNode, name, propName, options.enumSuffix)
441
+
442
+ const tupleNode = narrowSchema(schemaNode, 'tuple')
443
+ if (tupleNode?.items) {
444
+ const namedItems = tupleNode.items.map((item) => setEnumName(item, name, propName, options.enumSuffix))
445
+ if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
446
+ schemaNode = { ...tupleNode, items: namedItems }
447
+ }
448
+ }
729
449
 
730
450
  return createProperty({
731
451
  name: propName,
732
452
  schema: {
733
- ...convertSchema({ schema: resolvedPropSchema, name: derivedPropName }, options),
734
- nullable: propNullable || undefined,
735
- optional: !required && !propNullable ? true : undefined,
736
- nullish: !required && propNullable ? true : undefined,
453
+ ...schemaNode,
454
+ nullable: schemaNode.type === 'null' ? undefined : propNullable || undefined,
737
455
  },
738
456
  required,
739
457
  })
740
458
  })
741
459
  : []
742
460
 
743
- const additionalProperties = resolvedSchema.additionalProperties
461
+ const additionalProperties = schema.additionalProperties
744
462
  let additionalPropertiesNode: SchemaNode | true | undefined
745
463
  if (additionalProperties === true) {
746
464
  additionalPropertiesNode = true
747
465
  } else if (additionalProperties && Object.keys(additionalProperties).length > 0) {
748
- additionalPropertiesNode = convertSchema({ schema: additionalProperties as SchemaObject }, options)
466
+ additionalPropertiesNode = parseSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
749
467
  } else if (additionalProperties === false) {
750
468
  additionalPropertiesNode = undefined
751
469
  } else if (additionalProperties) {
752
- additionalPropertiesNode = createSchema({ type: resolveTypeOption(mergedOptions.unknownType) })
470
+ additionalPropertiesNode = createSchema({ type: typeOptionMap.get(options.unknownType)! })
753
471
  }
754
472
 
755
- const rawPatternProperties =
756
- 'patternProperties' in resolvedSchema ? (resolvedSchema as unknown as { patternProperties?: Record<string, SchemaObject> }).patternProperties : undefined
473
+ const rawPatternProperties = 'patternProperties' in schema ? schema.patternProperties : undefined
757
474
 
758
475
  const patternProperties = rawPatternProperties
759
476
  ? Object.fromEntries(
760
477
  Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [
761
478
  pattern,
762
- (patternSchema as unknown) === true || Object.keys(patternSchema as object).length === 0
763
- ? createSchema({ type: resolveTypeOption(mergedOptions.unknownType) })
764
- : convertSchema({ schema: patternSchema as SchemaObject }, options),
479
+ patternSchema === true || (typeof patternSchema === 'object' && Object.keys(patternSchema).length === 0)
480
+ ? createSchema({ type: typeOptionMap.get(options.unknownType)! })
481
+ : parseSchema({ schema: patternSchema as SchemaObject }, rawOptions),
765
482
  ]),
766
483
  )
767
484
  : undefined
768
485
 
769
- return createSchema({
486
+ const objectNode: SchemaNode = createSchema({
770
487
  type: 'object',
771
488
  primitive: 'object',
772
489
  properties,
773
490
  additionalProperties: additionalPropertiesNode,
774
491
  patternProperties,
775
- ...buildSchemaBase(schema, name, nullable, defaultValue),
492
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
776
493
  })
494
+
495
+ if (isDiscriminator(schema) && schema.discriminator.mapping) {
496
+ const discPropName = schema.discriminator.propertyName
497
+ const values = Object.keys(schema.discriminator.mapping)
498
+ const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : undefined
499
+ return setDiscriminatorEnum({ node: objectNode, propertyName: discPropName, values, enumName })
500
+ }
501
+
502
+ return objectNode
777
503
  }
778
504
 
779
505
  /**
780
506
  * Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
781
- *
782
- * Each `prefixItems` element maps to a positional tuple slot. An optional `items` schema
783
- * after the prefix items is mapped to the rest parameter of the tuple.
784
507
  */
785
- function convertTuple({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode {
786
- const rawSchema = schema as unknown as { prefixItems: SchemaObject[]; items?: SchemaObject }
787
- const tupleItems = rawSchema.prefixItems.map((item) => convertSchema({ schema: item }, options))
788
- const rest = rawSchema.items ? convertSchema({ schema: rawSchema.items }, options) : undefined
508
+ function convertTuple({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
509
+ const tupleItems = (schema.prefixItems ?? []).map((item) => parseSchema({ schema: item as SchemaObject }, rawOptions))
510
+ const rest = schema.items ? parseSchema({ schema: schema.items as SchemaObject }, rawOptions) : createSchema({ type: 'any' })
789
511
 
790
512
  return createSchema({
791
513
  type: 'tuple',
@@ -794,23 +516,17 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
794
516
  rest,
795
517
  min: schema.minItems,
796
518
  max: schema.maxItems,
797
- ...buildSchemaBase(schema, name, nullable, defaultValue),
519
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
798
520
  })
799
521
  }
800
522
 
801
523
  /**
802
524
  * Converts a `type: 'array'` schema into an `ArraySchemaNode`.
803
- *
804
- * When the items schema is an inline enum, a name derived from the parent array's name and
805
- * `enumSuffix` is forwarded so generators can emit a named enum declaration.
806
525
  */
807
- function convertArray({ schema, name, nullable, defaultValue, options, mergedOptions }: SchemaContext): SchemaNode {
808
- const rawSchema = schema as unknown as { items?: SchemaObject }
809
- // When the array items schema contains an inline enum, derive a name from the parent
810
- // array's name + enumSuffix so generators can emit a named enum declaration.
811
- const rawItems = rawSchema.items as SchemaObject | undefined
812
- const itemName = rawItems?.enum?.length && name ? pascalCase([name, mergedOptions.enumSuffix].join(' ')) : undefined
813
- const items = rawSchema.items ? [convertSchema({ schema: rawSchema.items, name: itemName }, options)] : []
526
+ function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): SchemaNode {
527
+ const rawItems = schema.items as SchemaObject | undefined
528
+ const itemName = rawItems?.enum?.length && name ? enumPropName(undefined, name, options.enumSuffix) : undefined
529
+ const items = rawItems ? [parseSchema({ schema: rawItems, name: itemName }, rawOptions)] : []
814
530
 
815
531
  return createSchema({
816
532
  type: 'array',
@@ -819,12 +535,12 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
819
535
  min: schema.minItems,
820
536
  max: schema.maxItems,
821
537
  unique: schema.uniqueItems ?? undefined,
822
- ...buildSchemaBase(schema, name, nullable, defaultValue),
538
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
823
539
  })
824
540
  }
825
541
 
826
542
  /**
827
- * Converts a `type: 'string'` schema (without a special format) into a `StringSchemaNode`.
543
+ * Converts a `type: 'string'` schema into a `StringSchemaNode`.
828
544
  */
829
545
  function convertString({ schema, name, nullable, defaultValue }: SchemaContext): SchemaNode {
830
546
  return createSchema({
@@ -833,12 +549,12 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
833
549
  min: schema.minLength,
834
550
  max: schema.maxLength,
835
551
  pattern: schema.pattern,
836
- ...buildSchemaBase(schema, name, nullable, defaultValue),
552
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
837
553
  })
838
554
  }
839
555
 
840
556
  /**
841
- * Converts a `type: 'number'` or `type: 'integer'` schema into the corresponding `SchemaNode`.
557
+ * Converts a `type: 'number'` or `type: 'integer'` schema.
842
558
  */
843
559
  function convertNumeric({ schema, name, nullable, defaultValue }: SchemaContext, type: 'number' | 'integer'): SchemaNode {
844
560
  return createSchema({
@@ -848,23 +564,23 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
848
564
  max: schema.maximum,
849
565
  exclusiveMinimum: typeof schema.exclusiveMinimum === 'number' ? schema.exclusiveMinimum : undefined,
850
566
  exclusiveMaximum: typeof schema.exclusiveMaximum === 'number' ? schema.exclusiveMaximum : undefined,
851
- ...buildSchemaBase(schema, name, nullable, defaultValue),
567
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
852
568
  })
853
569
  }
854
570
 
855
571
  /**
856
- * Converts a `type: 'boolean'` schema into a `BooleanSchemaNode`.
572
+ * Converts a `type: 'boolean'` schema.
857
573
  */
858
574
  function convertBoolean({ schema, name, nullable, defaultValue }: SchemaContext): SchemaNode {
859
575
  return createSchema({
860
576
  type: 'boolean',
861
577
  primitive: 'boolean',
862
- ...buildSchemaBase(schema, name, nullable, defaultValue),
578
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
863
579
  })
864
580
  }
865
581
 
866
582
  /**
867
- * Converts an explicit `type: 'null'` or `const: null` schema into a `NullSchemaNode`.
583
+ * Converts an explicit `type: 'null'` schema.
868
584
  */
869
585
  function convertNull({ schema, name, nullable }: SchemaContext): SchemaNode {
870
586
  return createSchema({
@@ -879,67 +595,42 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
879
595
  }
880
596
 
881
597
  /**
882
- * Central dispatcher: converts an OAS `SchemaObject` into a `SchemaNode`.
598
+ * Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
883
599
  *
884
- * Dispatch order (first match wins):
885
- * 1. `$ref` pointer
886
- * 2. `allOf` composition
887
- * 3. `oneOf` / `anyOf` union
888
- * 4. `const` literal (OAS 3.1)
889
- * 5. `format`-based special type (date/time, uuid, blob, …)
890
- * 6. OAS 3.1 `contentMediaType: 'application/octet-stream'` blob
891
- * 7. OAS 3.1 multi-type array → union or fallthrough
892
- * 8. Constraint-inferred type (minLength/maxLength → string; minimum/maximum → number)
893
- * 9. `enum` values
894
- * 10. Object / array / tuple / scalar by `type`
895
- * 11. Empty schema fallback (`emptySchemaType` option)
600
+ * Dispatch order (first match wins): `$ref` → `allOf` → `oneOf`/`anyOf` → `const` → `format`
601
+ * octet-stream blob → multi-type array → constraint-inferred type → `enum` → object/array/tuple/scalar
602
+ * empty-schema fallback (`emptySchemaType` option).
896
603
  */
897
- function convertSchema({ schema, name }: { schema: SchemaObject; name?: string }, options?: Partial<Options>): SchemaNode {
898
- const mergedOptions: Options = { ...DEFAULT_OPTIONS, ...options }
899
- // Flatten keyword-only allOf fragments (no $ref, no structural keys) into the parent
900
- // schema before parsing, so simple annotation patterns don't produce needless intersections.
901
- const flattenedSchema = flattenSchema(schema as unknown as Parameters<typeof flattenSchema>[0]) as SchemaObject | null
902
- if (flattenedSchema && flattenedSchema !== (schema as unknown)) {
903
- return convertSchema({ schema: flattenedSchema, name }, options)
604
+ function parseSchema({ schema, name }: { schema: SchemaObject; name?: string | null }, rawOptions?: Partial<ParserOptions>): SchemaNode {
605
+ const options: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...rawOptions }
606
+ const flattenedSchema = flattenSchema(schema)
607
+ if (flattenedSchema && flattenedSchema !== schema) {
608
+ return parseSchema({ schema: flattenedSchema, name }, rawOptions)
904
609
  }
905
610
 
906
611
  const nullable = isNullable(schema) || undefined
907
612
  const defaultValue = schema.default === null && nullable ? undefined : schema.default
908
- // Normalize OAS 3.1 multi-type array to a single type string for the dispatch below.
909
613
  const type = Array.isArray(schema.type) ? schema.type[0] : schema.type
910
614
 
911
- const ctx: SchemaContext = { schema, name, nullable, defaultValue, type, options, mergedOptions }
615
+ const ctx: SchemaContext = { schema, name, nullable, defaultValue, type, rawOptions, options }
912
616
 
913
- // $ref — pointer to another definition.
914
- // In OAS 3.0 siblings of $ref are technically ignored, but Kubb intentionally preserves them
915
- // so that annotations like `pattern`, `description`, and `nullable` are reflected in generated code.
916
617
  if (isReference(schema)) return convertRef(ctx)
917
618
 
918
- // Composition keywords
919
619
  if (schema.allOf?.length) return convertAllOf(ctx)
920
620
  const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
921
621
  if (unionMembers.length) return convertUnion(ctx)
922
622
 
923
- // OAS 3.1 const — a single fixed value, semantically equivalent to a one-item enum.
924
- // `const: undefined` falls through to the empty-type fallback.
925
623
  if ('const' in schema && schema.const !== undefined) return convertConst(ctx)
926
624
 
927
- // Format-based special types take precedence over `type`.
928
- // `convertFormat` returns undefined when format should fall through to string (dateType: false).
929
- // see https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7
930
625
  if (schema.format) {
931
626
  const formatResult = convertFormat(ctx)
932
627
  if (formatResult) return formatResult
933
628
  }
934
629
 
935
- // OAS 3.1: `contentMediaType: 'application/octet-stream'` on a string schema signals binary data.
936
- if (schema.type === 'string' && (schema as SchemaObject & { contentMediaType?: string }).contentMediaType === 'application/octet-stream') {
937
- return createSchema({ type: 'blob', primitive: 'string', ...buildSchemaBase(schema, name, nullable, defaultValue) })
630
+ if (schema.type === 'string' && schema.contentMediaType === 'application/octet-stream') {
631
+ return createSchema({ type: 'blob', primitive: 'string', ...buildSchemaNode(schema, name, nullable, defaultValue) })
938
632
  }
939
633
 
940
- // OAS 3.1: `type` may be an array — e.g. `["string", "integer", "null"]`.
941
- // `null` in the array is the 3.1 equivalent of `nullable: true`; strip it and set the flag.
942
- // When 2+ non-null types remain, produce a union; when exactly 1 non-null type remains, fall through.
943
634
  if (Array.isArray(schema.type) && schema.type.length > 1) {
944
635
  const nonNullTypes = schema.type.filter((t) => t !== 'null') as string[]
945
636
  const arrayNullable = schema.type.includes('null') || nullable || undefined
@@ -947,15 +638,12 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
947
638
  if (nonNullTypes.length > 1) {
948
639
  return createSchema({
949
640
  type: 'union',
950
- members: nonNullTypes.map((t) => convertSchema({ schema: { ...schema, type: t } as SchemaObject }, options)),
951
- ...buildSchemaBase(schema, name, arrayNullable, defaultValue),
641
+ members: nonNullTypes.map((t) => parseSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
642
+ ...buildSchemaNode(schema, name, arrayNullable, defaultValue),
952
643
  })
953
644
  }
954
645
  }
955
646
 
956
- // Infer type from constraints when no explicit type is provided.
957
- // minLength / maxLength / pattern → string; minimum / maximum → number.
958
- // Note: minItems/maxItems do NOT infer array — arrays require an `items` key.
959
647
  if (!type) {
960
648
  if (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined) {
961
649
  return convertString(ctx)
@@ -975,47 +663,69 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
975
663
  if (type === 'boolean') return convertBoolean(ctx)
976
664
  if (type === 'null') return convertNull(ctx)
977
665
 
978
- const emptyType = resolveTypeOption(mergedOptions.emptySchemaType)
666
+ const emptyType = typeOptionMap.get(options.emptySchemaType)!
979
667
  return createSchema({ type: emptyType as ScalarSchemaType, name, title: schema.title, description: schema.description })
980
668
  }
981
669
 
982
670
  /**
983
- * Converts a single dereferenced OAS parameter object into a `ParameterNode`.
984
- * When the parameter has no `schema` or its schema is a `$ref`, falls back to `unknownType`.
671
+ * Converts a dereferenced OAS parameter object into a `ParameterNode`.
985
672
  */
986
- function parseParameter(options: Options, param: Record<string, unknown>): ParameterNode {
987
- const schema =
988
- param['schema'] && !isReference(param['schema'] as object)
989
- ? convertSchema({ schema: param['schema'] as SchemaObject }, options)
990
- : createSchema({ type: resolveTypeOption(options.unknownType) })
673
+ function parseParameter(options: ParserOptions, param: Record<string, unknown>): ParameterNode {
674
+ const required = (param['required'] as boolean | undefined) ?? false
675
+
676
+ const schema: SchemaNode = param['schema']
677
+ ? parseSchema({ schema: param['schema'] as SchemaObject }, options)
678
+ : createSchema({ type: typeOptionMap.get(options.unknownType)! })
991
679
 
992
680
  return createParameter({
993
681
  name: param['name'] as string,
994
682
  in: param['in'] as ParameterLocation,
995
- schema,
996
- required: (param['required'] as boolean | undefined) ?? false,
683
+ schema: {
684
+ ...schema,
685
+ description: (param['description'] as string | undefined) ?? schema.description,
686
+ },
687
+ required,
997
688
  })
998
689
  }
999
690
 
1000
691
  /**
1001
- * Converts an OAS `Operation` into an `OperationNode`, resolving parameters,
1002
- * request body, and all response codes into their AST node equivalents.
692
+ * Converts an OAS `Operation` into an `OperationNode`.
1003
693
  */
1004
- function parseOperation(options: Options, oas: Oas, operation: Operation): OperationNode {
1005
- const parameters: Array<ParameterNode> = operation.getParameters().map((param) => {
1006
- const dereferenced = oas.dereferenceWithRef(param) as unknown as Record<string, unknown>
694
+ function parseOperation(options: ParserOptions, operation: Operation): OperationNode {
695
+ const parameters: Array<ParameterNode> = getParameters(document, operation).map((param) =>
696
+ parseParameter(options, param as unknown as Record<string, unknown>),
697
+ )
1007
698
 
1008
- return parseParameter(options, dereferenced)
1009
- })
699
+ const requestBodySchema = getRequestSchema(document, operation, { contentType: ctx.contentType })
700
+ const requestBodySchemaNode = requestBodySchema ? parseSchema({ schema: requestBodySchema }, options) : undefined
1010
701
 
1011
- const requestBodySchema = oas.getRequestSchema(operation)
1012
- const requestBody = requestBodySchema ? convertSchema({ schema: requestBodySchema }, options) : undefined
702
+ const requestBodyDescription =
703
+ operation.schema.requestBody && !isReference(operation.schema.requestBody)
704
+ ? (operation.schema.requestBody as { description?: string }).description
705
+ : undefined
706
+
707
+ const requestBodyKeysToOmit = requestBodySchema?.properties
708
+ ? Object.entries(requestBodySchema.properties)
709
+ .filter(([, prop]) => !isReference(prop) && (prop as { readOnly?: boolean }).readOnly)
710
+ .map(([key]) => key)
711
+ : undefined
712
+
713
+ const requestBody = requestBodySchemaNode
714
+ ? {
715
+ description: requestBodyDescription,
716
+ schema: requestBodySchemaNode,
717
+ keysToOmit: requestBodyKeysToOmit?.length ? requestBodyKeysToOmit : undefined,
718
+ }
719
+ : undefined
1013
720
 
1014
721
  const responses: Array<ResponseNode> = operation.getResponseStatusCodes().map((statusCode) => {
1015
722
  const responseObj = operation.getResponseByStatusCode(statusCode)
1016
- const responseSchema = oas.getResponseSchema(operation, statusCode)
723
+ const responseSchema = getResponseSchema(document, operation, statusCode, { contentType: ctx.contentType })
1017
724
 
1018
- const schema = responseSchema && Object.keys(responseSchema).length > 0 ? convertSchema({ schema: responseSchema }, options) : undefined
725
+ const schema =
726
+ responseSchema && Object.keys(responseSchema).length > 0
727
+ ? parseSchema({ schema: responseSchema }, options)
728
+ : createSchema({ type: typeOptionMap.get(options.emptySchemaType)! })
1019
729
 
1020
730
  const description = typeof responseObj === 'object' && responseObj !== null && !Array.isArray(responseObj) ? responseObj.description : undefined
1021
731
 
@@ -1024,20 +734,27 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
1024
734
  ? (responseObj as { content?: Record<string, unknown> }).content
1025
735
  : undefined
1026
736
 
1027
- const mediaType = rawContent ? toMediaType(Object.keys(rawContent)[0] ?? '') : toMediaType(operation.contentType ?? '')
737
+ const mediaType = rawContent ? getMediaType(Object.keys(rawContent)[0] ?? '') : getMediaType(operation.contentType ?? '')
738
+
739
+ const keysToOmit = responseSchema?.properties
740
+ ? Object.entries(responseSchema.properties)
741
+ .filter(([, prop]) => !isReference(prop) && (prop as { writeOnly?: boolean }).writeOnly)
742
+ .map(([key]) => key)
743
+ : undefined
1028
744
 
1029
745
  return createResponse({
1030
746
  statusCode: statusCode as StatusCode,
1031
747
  description,
1032
748
  schema,
1033
749
  mediaType,
750
+ keysToOmit: keysToOmit?.length ? keysToOmit : undefined,
1034
751
  })
1035
752
  })
1036
753
 
1037
754
  return createOperation({
1038
755
  operationId: operation.getOperationId(),
1039
756
  method: operation.method.toUpperCase() as HttpMethod,
1040
- path: operation.path,
757
+ path: new URLPath(operation.path).URL,
1041
758
  tags: operation.getTags().map((tag) => tag.name),
1042
759
  summary: operation.getSummary() || undefined,
1043
760
  description: operation.getDescription() || undefined,
@@ -1048,89 +765,57 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
1048
765
  })
1049
766
  }
1050
767
 
1051
- /**
1052
- * Converts an OpenAPI/Swagger spec (wrapped in a Kubb `Oas` instance) into
1053
- * a `RootNode` — the top-level node of the `@kubb/ast` tree.
1054
- */
1055
- function parse<TOptions extends Partial<Options> = object>(options?: TOptions): RootNode {
1056
- const mergedOptions: Options = { ...DEFAULT_OPTIONS, ...options }
1057
-
1058
- const schemas: Array<SchemaNode> = Object.entries(schemaObjects).map(([name, schemaObject]) =>
1059
- convertSchema({ schema: schemaObject as SchemaObject, name }, mergedOptions),
1060
- )
1061
-
1062
- const paths = oas.getPaths()
1063
-
1064
- const operations: Array<OperationNode> = Object.entries(paths).flatMap(([_path, methods]) =>
1065
- Object.entries(methods)
1066
- .map(([, operation]) => (operation ? parseOperation(mergedOptions, oas, operation) : null))
1067
- .filter((op): op is OperationNode => op !== null),
1068
- )
768
+ return { parseSchema, parseOperation, parseParameter }
769
+ }
1069
770
 
1070
- return createRoot({ schemas, operations })
1071
- }
771
+ /**
772
+ * Converts a single `SchemaObject` into a `SchemaNode`.
773
+ *
774
+ * @example
775
+ * ```ts
776
+ * const ctx = { document }
777
+ * parseSchema(ctx, { schema: { type: 'string', format: 'uuid' } })
778
+ * ```
779
+ */
780
+ export function parseSchema(ctx: OasParserContext, { schema, name }: { schema: SchemaObject; name?: string }, options?: Partial<ParserOptions>): SchemaNode {
781
+ return createSchemaParser(ctx).parseSchema({ schema, name }, options)
782
+ }
1072
783
 
1073
- /**
1074
- * Walks a `SchemaNode` tree and resolves all `ref` node names through the provided callbacks.
1075
- *
1076
- * `resolveName` handles all schema types; `resolveEnumName` (when provided) takes precedence
1077
- * for `enum` nodes, enabling a separate naming strategy for enums (e.g. different suffix).
1078
- *
1079
- * Collision-resolved names (from `nameMapping`) are applied before user-supplied resolvers.
1080
- */
1081
- function resolveRefs(node: SchemaNode, resolveName: (ref: string) => string | undefined, resolveEnumName?: (name: string) => string | undefined): SchemaNode {
1082
- return transform(node, {
1083
- schema(schemaNode) {
1084
- const schemaRef = narrowSchema(schemaNode, schemaTypes.ref)
1085
-
1086
- if (schemaRef && (schemaRef.ref || schemaRef.name)) {
1087
- const rawRef = schemaRef.ref ?? schemaRef.name!
1088
- const resolved = resolveName(nameMapping.get(rawRef) ?? rawRef)
1089
- if (resolved) {
1090
- return { ...schemaNode, name: resolved }
1091
- }
1092
- }
784
+ /**
785
+ * Converts the entire OpenAPI spec into a `RootNode` (the top-level `@kubb/ast` tree).
786
+ *
787
+ * This is the main entry point: `OpenAPI / Swagger Kubb AST`.
788
+ * No code is generated here the resulting tree is spec-agnostic and consumed by
789
+ * downstream plugins (`plugin-ts`, `plugin-zod`, …).
790
+ *
791
+ * @example
792
+ * ```ts
793
+ * const document = await parseFromConfig(config)
794
+ * const root = parseOas(document, { dateType: 'date', contentType: 'application/json' })
795
+ * ```
796
+ */
797
+ export function parseOas(
798
+ document: Document,
799
+ options: Partial<ParserOptions> & { contentType?: contentType } = {},
800
+ ): { root: RootNode; nameMapping: Map<string, string> } {
801
+ const { contentType, ...parserOptions } = options
802
+ const mergedOptions: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...parserOptions }
1093
803
 
1094
- if (schemaNode.type === 'enum' && schemaNode.name) {
1095
- const resolved = (resolveEnumName ?? resolveName)(schemaNode.name)
1096
- if (resolved) {
1097
- return { ...schemaNode, name: resolved }
1098
- }
1099
- }
1100
- },
1101
- }) as SchemaNode
1102
- }
804
+ const { schemas: schemaObjects, nameMapping } = getSchemas(document, { contentType })
805
+ const { parseSchema: _parseSchema, parseOperation: _parseOperation } = createSchemaParser({ document, contentType })
1103
806
 
1104
- /**
1105
- * Collects all `KubbFile.Import` descriptors needed by a `SchemaNode` tree.
1106
- *
1107
- * Walks the tree looking for `ref` nodes, verifies each `$ref` is resolvable in the spec,
1108
- * applies collision-resolved names from `nameMapping`, and calls `resolve` to obtain the
1109
- * import path and name. Returns an empty array for refs that cannot be resolved.
1110
- */
1111
- function getImports(node: SchemaNode, resolve: (schemaName: string) => { name: string; path: string } | undefined): Array<KubbFile.Import> {
1112
- return collect<KubbFile.Import>(node, {
1113
- schema(schemaNode): KubbFile.Import | undefined {
1114
- if (schemaNode.type !== 'ref' || !schemaNode.ref) return
1115
- // Use the OAS instance to verify this $ref is importable (exists in the spec).
1116
- if (!oas.get(schemaNode.ref)) return
807
+ const schemas: Array<SchemaNode> = Object.entries(schemaObjects).map(([name, schema]) => _parseSchema({ schema, name }, mergedOptions))
1117
808
 
1118
- const rawName = extractRefName(schemaNode.ref)
809
+ const baseOas = new BaseOas(document)
810
+ const paths = baseOas.getPaths()
1119
811
 
1120
- // Apply collision-resolved name if available.
1121
- const schemaName = nameMapping.get(rawName) ?? rawName
1122
- const result = resolve(schemaName)
1123
- if (!result) return
812
+ const operations: Array<OperationNode> = Object.entries(paths).flatMap(([_path, methods]) =>
813
+ Object.entries(methods)
814
+ .map(([, operation]) => (operation ? _parseOperation(mergedOptions, operation) : null))
815
+ .filter((op): op is OperationNode => op !== null),
816
+ )
1124
817
 
1125
- return { name: [result.name], path: result.path }
1126
- },
1127
- })
1128
- }
818
+ const root = createRoot({ schemas, operations })
1129
819
 
1130
- return {
1131
- parse: parse,
1132
- convertSchema,
1133
- resolveRefs,
1134
- getImports,
1135
- } as OasParser
820
+ return { root, nameMapping }
1136
821
  }