@kubb/adapter-oas 5.0.0-alpha.4 → 5.0.0-alpha.41

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