@kubb/adapter-oas 5.0.0-alpha.3 → 5.0.0-alpha.30

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,65 @@
1
- import { pascalCase, URLPath } from '@internals/utils'
2
- import { createOperation, createParameter, createProperty, createResponse, createRoot, createSchema, narrowSchema, schemaTypes, transform } from '@kubb/ast'
1
+ import { URLPath } from '@internals/utils'
2
+ import {
3
+ childName,
4
+ createDiscriminantNode,
5
+ createOperation,
6
+ createParameter,
7
+ createProperty,
8
+ createResponse,
9
+ createRoot,
10
+ createSchema,
11
+ enumPropName,
12
+ extractRefName,
13
+ findDiscriminator,
14
+ mergeAdjacentObjects,
15
+ narrowSchema,
16
+ type ParserOptions,
17
+ setDiscriminatorEnum,
18
+ setEnumName,
19
+ simplifyUnion,
20
+ syncOptionality,
21
+ } from '@kubb/ast'
3
22
  import type {
4
- ArraySchemaNode,
5
- DateSchemaNode,
6
- DatetimeSchemaNode,
7
- EnumSchemaNode,
23
+ DistributiveOmit,
8
24
  HttpMethod,
9
- IntersectionSchemaNode,
10
- MediaType,
11
- NumberSchemaNode,
12
- ObjectSchemaNode,
13
25
  OperationNode,
14
26
  ParameterLocation,
15
27
  ParameterNode,
16
28
  PrimitiveSchemaType,
17
29
  PropertyNode,
18
- RefSchemaNode,
19
30
  ResponseNode,
20
31
  RootNode,
21
- ScalarSchemaNode,
22
32
  ScalarSchemaType,
23
33
  SchemaNode,
24
- SchemaType,
25
34
  StatusCode,
26
- StringSchemaNode,
27
- TimeSchemaNode,
28
- UnionSchemaNode,
29
35
  } 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'
36
+ import BaseOas from 'oas'
37
+ import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, typeOptionMap } from './constants.ts'
38
+ import { isDiscriminator, isNullable, isReference } from './guards.ts'
39
+ import { resolveRef } from './refs.ts'
40
+ import {
41
+ buildSchemaNode,
42
+ flattenSchema,
43
+ getDateType,
44
+ getMediaType,
45
+ getParameters,
46
+ getPrimitiveType,
47
+ getRequestSchema,
48
+ getResponseSchema,
49
+ getSchemas,
50
+ getSchemaType,
51
+ } from './resolvers.ts'
52
+ import type { ContentType, Document, Operation, ReferenceObject, SchemaObject } from './types.ts'
35
53
 
36
54
  /**
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`.
55
+ * Construction-time context for the OAS parser.
56
+ *
57
+ * Holds the raw OpenAPI document and optional content-type override used when extracting
58
+ * request/response schemas.
184
59
  */
185
- function toMediaType(contentType: string): MediaType | undefined {
186
- return knownMediaTypes.includes(contentType as MediaType) ? (contentType as MediaType) : undefined
60
+ export type OasParserContext = {
61
+ document: Document
62
+ contentType?: ContentType
187
63
  }
188
64
 
189
65
  /**
@@ -192,178 +68,84 @@ function toMediaType(contentType: string): MediaType | undefined {
192
68
  */
193
69
  type SchemaContext = {
194
70
  schema: SchemaObject
195
- name: string | undefined
71
+ name: string | null | undefined
196
72
  nullable: true | undefined
197
73
  defaultValue: unknown
198
74
  /**
199
75
  * Normalized single type string (first element when OAS 3.1 multi-type array).
200
76
  */
201
77
  type: string | undefined
202
- options: Partial<Options> | undefined
203
- mergedOptions: Options
78
+ rawOptions: Partial<ParserOptions> | undefined
79
+ options: ParserOptions
204
80
  }
205
81
 
206
82
  /**
207
- * The public interface returned by `createOasParser`.
83
+ * Normalize a malformed `{ type: 'array', enum: [...] }` schema by moving the
84
+ * enum values into the items sub-schema. This pattern is technically invalid OAS
85
+ * but appears in the wild and must be handled gracefully.
208
86
  */
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
87
+ function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
88
+ const isItemsObject = typeof schema.items === 'object' && !Array.isArray(schema.items)
89
+ const normalizedItems: SchemaObject = { ...(isItemsObject ? (schema.items as SchemaObject) : {}), enum: schema.enum }
90
+ const { enum: _enum, ...schemaWithoutEnum } = schema
228
91
 
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>
92
+ return { ...schemaWithoutEnum, items: normalizedItems } as SchemaObject
237
93
  }
238
94
 
239
95
  /**
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
248
- *
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, …).
96
+ * Builds the internal converter functions for a given `OasParserContext`.
251
97
  *
252
- * @example
253
- * ```ts
254
- * const parser = createOasParser(oas)
255
- * const root = parser.parse({ emptySchemaType: 'unknown' })
256
- * ```
98
+ * All `convert*` functions are defined as function declarations so they can freely
99
+ * reference each other and `parseSchema` via JS hoisting (mutual recursion).
257
100
  */
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 })
101
+ function createSchemaParser(ctx: OasParserContext) {
102
+ const document = ctx.document
103
+
104
+ // Branch handlers each converts one OAS schema pattern to a SchemaNode.
262
105
 
263
106
  /**
264
- * Maps an `'any' | 'unknown' | 'void'` option string to the corresponding `SchemaType` constant.
265
- * Used for both `unknownType` (unannotated schemas) and `emptySchemaType` (empty `{}` schemas).
107
+ * Tracks `$ref` paths that are currently being resolved to prevent infinite
108
+ * recursion when schemas contain circular references (e.g. `Pet parent → Pet`).
266
109
  */
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
- }
110
+ const resolvingRefs = new Set<string>()
272
111
 
273
112
  /**
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`.
113
+ * Converts a `$ref` schema into a `RefSchemaNode`.
114
+ *
115
+ * The resolved schema is stored in `node.schema`. Usage-site sibling fields
116
+ * (description, readOnly, nullable, etc.) are stored directly on the ref node.
117
+ * Use `syncSchemaRef(node)` in printers to get a merged view of both.
118
+ * Circular refs are detected via `resolvingRefs` and leave `schema` as `undefined`.
276
119
  */
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 }
120
+ function convertRef({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
121
+ let resolvedSchema: SchemaNode | undefined
122
+ const refPath = schema.$ref
123
+ if (refPath && !resolvingRefs.has(refPath)) {
124
+ try {
125
+ const referenced = resolveRef<SchemaObject>(document, refPath)
126
+ if (referenced) {
127
+ resolvingRefs.add(refPath)
128
+ resolvedSchema = parseSchema({ schema: referenced }, rawOptions)
129
+ resolvingRefs.delete(refPath)
130
+ }
131
+ } catch {
132
+ // Ref cannot be resolved in this document (e.g. unit tests with minimal documents).
294
133
  }
295
- return { type: 'datetime', offset: false }
296
134
  }
297
135
 
298
- if (format === 'date') {
299
- return { type: 'date', representation: options.dateType === 'date' ? 'date' : 'string' }
300
- }
301
-
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
136
  return createSchema({
137
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
337
138
  type: 'ref',
338
139
  name: extractRefName(schema.$ref!),
339
140
  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,
141
+ schema: resolvedSchema,
348
142
  })
349
143
  }
350
144
 
351
145
  /**
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.
146
+ * Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
365
147
  */
366
- function convertAllOf({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode {
148
+ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
367
149
  if (
368
150
  schema.allOf!.length === 1 &&
369
151
  !schema.properties &&
@@ -371,7 +153,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
371
153
  schema.additionalProperties === undefined
372
154
  ) {
373
155
  const [memberSchema] = schema.allOf as Array<SchemaObject | ReferenceObject>
374
- const memberNode = convertSchema({ schema: memberSchema! as SchemaObject }, options)
156
+ const memberNode = parseSchema({ schema: memberSchema! as SchemaObject, name: null }, rawOptions)
375
157
  const { kind: _kind, ...memberNodeProps } = memberNode
376
158
  const mergedNullable = nullable || memberNode.nullable || undefined
377
159
  const mergedDefault = schema.default === null && mergedNullable ? undefined : (schema.default ?? memberNode.default)
@@ -391,28 +173,30 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
391
173
  } as DistributiveOmit<SchemaNode, 'kind'>)
392
174
  }
393
175
 
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.
176
+ const filteredDiscriminantValues: Array<{ propertyName: string; value: string }> = []
396
177
  const allOfMembers: Array<SchemaNode> = (schema.allOf as Array<SchemaObject | ReferenceObject>)
397
178
  .filter((item) => {
398
179
  if (!isReference(item) || !name) return true
399
- const deref = oas.get<SchemaObject>(item.$ref)
180
+ const deref = resolveRef<SchemaObject>(document, item.$ref)
400
181
  if (!deref || !isDiscriminator(deref)) return true
401
182
  const parentUnion = deref.oneOf ?? deref.anyOf
402
183
  if (!parentUnion) return true
403
184
  const childRef = `#/components/schemas/${name}`
404
185
  const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef)
405
186
  const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef)
406
- return !inOneOf && !inMapping
187
+ if (inOneOf || inMapping) {
188
+ const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef)
189
+ if (discriminatorValue) {
190
+ filteredDiscriminantValues.push({ propertyName: deref.discriminator.propertyName, value: discriminatorValue })
191
+ }
192
+ return false
193
+ }
194
+ return true
407
195
  })
408
- .map((s) => convertSchema({ schema: s as SchemaObject }, options))
196
+ .map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))
409
197
 
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
198
  const syntheticStart = allOfMembers.length
413
199
 
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
200
  if (Array.isArray(schema.required) && schema.required.length) {
417
201
  const outerKeys = schema.properties ? new Set(Object.keys(schema.properties)) : new Set<string>()
418
202
  const missingRequired = schema.required.filter((key) => !outerKeys.has(key))
@@ -420,14 +204,14 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
420
204
  if (missingRequired.length) {
421
205
  const resolvedMembers = (schema.allOf as Array<SchemaObject | ReferenceObject>).flatMap((item) => {
422
206
  if (!isReference(item)) return [item as SchemaObject]
423
- const deref = oas.get<SchemaObject>(item.$ref)
207
+ const deref = resolveRef<SchemaObject>(document, item.$ref)
424
208
  return deref && !isReference(deref) ? [deref] : []
425
209
  })
426
210
 
427
211
  for (const key of missingRequired) {
428
212
  for (const resolved of resolvedMembers) {
429
213
  if (resolved.properties?.[key]) {
430
- allOfMembers.push(convertSchema({ schema: { properties: { [key]: resolved.properties[key] }, required: [key] } as SchemaObject }, options))
214
+ allOfMembers.push(parseSchema({ schema: { properties: { [key]: resolved.properties[key] }, required: [key] } as SchemaObject }, rawOptions))
431
215
  break
432
216
  }
433
217
  }
@@ -437,73 +221,108 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
437
221
 
438
222
  if (schema.properties) {
439
223
  const { allOf: _allOf, ...schemaWithoutAllOf } = schema
440
- allOfMembers.push(convertSchema({ schema: schemaWithoutAllOf }, options))
224
+ allOfMembers.push(parseSchema({ schema: schemaWithoutAllOf }, rawOptions))
225
+ }
226
+
227
+ for (const { propertyName, value } of filteredDiscriminantValues) {
228
+ allOfMembers.push(createDiscriminantNode({ propertyName, value }))
441
229
  }
442
230
 
443
- // Merge consecutive anonymous object members within the synthetic portion — see `mergeAdjacentAnonymousObjects`.
444
231
  return createSchema({
445
232
  type: 'intersection',
446
- members: [...allOfMembers.slice(0, syntheticStart), ...mergeAdjacentAnonymousObjects(allOfMembers.slice(syntheticStart))],
447
- ...buildSchemaBase(schema, name, nullable, defaultValue),
233
+ members: [...mergeAdjacentObjects(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentObjects(allOfMembers.slice(syntheticStart))],
234
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
448
235
  })
449
236
  }
450
237
 
451
238
  /**
452
239
  * 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
240
  */
459
- function convertUnion({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode {
241
+ function convertUnion({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
242
+ function pickDiscriminatorPropertyNode(node: SchemaNode, propertyName: string): SchemaNode | null {
243
+ const objectNode = narrowSchema(node, 'object')
244
+ const discriminatorProperty = objectNode?.properties?.find((property) => property.name === propertyName)
245
+
246
+ if (!discriminatorProperty) {
247
+ return null
248
+ }
249
+
250
+ return createSchema({
251
+ type: 'object',
252
+ primitive: 'object',
253
+ properties: [discriminatorProperty],
254
+ })
255
+ }
256
+
460
257
  const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
461
258
  const unionBase = {
462
- ...buildSchemaBase(schema, name, nullable, defaultValue),
259
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
463
260
  discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
464
261
  }
262
+ const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
263
+ const sharedPropertiesNode = schema.properties
264
+ ? (() => {
265
+ const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
266
+ const memberBaseSchema: SchemaObject = discriminator
267
+ ? (Object.fromEntries(Object.entries(schemaWithoutUnion).filter(([key]) => key !== 'discriminator')) as SchemaObject)
268
+ : schemaWithoutUnion
269
+ return parseSchema({ schema: memberBaseSchema, name }, rawOptions)
270
+ })()
271
+ : undefined
465
272
 
466
- if (schema.properties) {
467
- const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
468
- const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
273
+ if (sharedPropertiesNode || discriminator?.mapping) {
274
+ const members = unionMembers.map((s) => {
275
+ const ref = isReference(s) ? s.$ref : undefined
276
+ const discriminatorValue = findDiscriminator(discriminator?.mapping, ref)
277
+ const memberNode = parseSchema({ schema: s as SchemaObject }, rawOptions)
278
+
279
+ if (!discriminatorValue || !discriminator) {
280
+ return memberNode
281
+ }
469
282
 
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
283
+ const narrowedDiscriminatorNode = sharedPropertiesNode
284
+ ? pickDiscriminatorPropertyNode(
285
+ setDiscriminatorEnum({
286
+ node: sharedPropertiesNode,
287
+ propertyName: discriminator.propertyName,
288
+ values: [discriminatorValue],
289
+ }),
290
+ discriminator.propertyName,
291
+ )
292
+ : undefined
474
293
 
475
- return createSchema({
294
+ return createSchema({
295
+ type: 'intersection',
296
+ members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({ propertyName: discriminator.propertyName, value: discriminatorValue })],
297
+ })
298
+ })
299
+
300
+ const unionNode = createSchema({
476
301
  type: 'union',
477
302
  ...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)
303
+ members,
304
+ })
483
305
 
484
- if (discriminatorValue && discriminator) {
485
- propertiesNode = applyDiscriminatorEnum({ node: propertiesNode, propertyName: discriminator.propertyName, values: [discriminatorValue] })
486
- }
306
+ if (!sharedPropertiesNode) {
307
+ return unionNode
308
+ }
487
309
 
488
- return createSchema({
489
- type: 'intersection',
490
- members: [convertSchema({ schema: s as SchemaObject }, options), propertiesNode],
491
- })
492
- }),
310
+ return createSchema({
311
+ type: 'intersection',
312
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
313
+ members: [unionNode, sharedPropertiesNode],
493
314
  })
494
315
  }
495
316
 
496
317
  return createSchema({
497
318
  type: 'union',
498
319
  ...unionBase,
499
- members: simplifyUnionMembers(unionMembers.map((s) => convertSchema({ schema: s as SchemaObject }, options))),
320
+ members: simplifyUnion(unionMembers.map((s) => parseSchema({ schema: s as SchemaObject }, rawOptions))),
500
321
  })
501
322
  }
502
323
 
503
324
  /**
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.
325
+ * Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
507
326
  */
508
327
  function convertConst({ schema, name, nullable, defaultValue }: SchemaContext): SchemaNode {
509
328
  const constValue = schema.const
@@ -516,7 +335,6 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
516
335
  title: schema.title,
517
336
  description: schema.description,
518
337
  deprecated: schema.deprecated,
519
- nullable,
520
338
  })
521
339
  }
522
340
 
@@ -525,22 +343,20 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
525
343
  type: 'enum',
526
344
  primitive: constPrimitive,
527
345
  enumValues: [constValue as string | number | boolean],
528
- ...buildSchemaBase(schema, name, nullable, defaultValue),
346
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
529
347
  })
530
348
  }
531
349
 
532
350
  /**
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`).
351
+ * Converts a format-annotated schema into a special-type `SchemaNode`.
352
+ * Returns `null` when the format should fall through to string handling (`dateType: false`).
536
353
  */
537
- function convertFormat({ schema, name, nullable, defaultValue, mergedOptions }: SchemaContext): SchemaNode | undefined {
538
- const base = buildSchemaBase(schema, name, nullable, defaultValue)
354
+ function convertFormat({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode | null {
355
+ const base = buildSchemaNode(schema, name, nullable, defaultValue)
539
356
 
540
- // int64 is option-dependent so it can't live in the static formatMap.
541
357
  if (schema.format === 'int64') {
542
358
  return createSchema({
543
- type: mergedOptions.integerType === 'bigint' ? 'bigint' : 'integer',
359
+ type: options.integerType === 'bigint' ? 'bigint' : 'integer',
544
360
  primitive: 'integer',
545
361
  ...base,
546
362
  min: schema.minimum,
@@ -550,10 +366,9 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
550
366
  })
551
367
  }
552
368
 
553
- // date-time / date / time are option-dependent and can't live in the static formatMap.
554
369
  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
370
+ const dateType = getDateType(options, schema.format)
371
+ if (!dateType) return null
557
372
 
558
373
  if (dateType.type === 'datetime') {
559
374
  return createSchema({ ...base, primitive: 'string' as const, type: 'datetime', offset: dateType.offset, local: dateType.local })
@@ -561,37 +376,38 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
561
376
  return createSchema({ ...base, primitive: 'string' as const, type: dateType.type, representation: dateType.representation })
562
377
  }
563
378
 
564
- const specialType = formatToSchemaType(schema.format!)
565
- if (!specialType) return undefined
379
+ const specialType = getSchemaType(schema.format!)
380
+ if (!specialType) return null
566
381
 
567
382
  const specialPrimitive: PrimitiveSchemaType = specialType === 'number' || specialType === 'integer' || specialType === 'bigint' ? specialType : 'string'
568
383
 
569
384
  if (specialType === 'number' || specialType === 'integer' || specialType === 'bigint') {
570
385
  return createSchema({ ...base, primitive: specialPrimitive, type: specialType })
571
386
  }
387
+ if (specialType === 'url') {
388
+ return createSchema({ ...base, primitive: 'string' as const, type: 'url', min: schema.minLength, max: schema.maxLength })
389
+ }
390
+ if (specialType === 'ipv4') {
391
+ return createSchema({ ...base, primitive: 'string' as const, type: 'ipv4' })
392
+ }
393
+ if (specialType === 'ipv6') {
394
+ return createSchema({ ...base, primitive: 'string' as const, type: 'ipv6' })
395
+ }
396
+ if (specialType === 'uuid' || specialType === 'email') {
397
+ return createSchema({ ...base, primitive: 'string' as const, type: specialType, min: schema.minLength, max: schema.maxLength })
398
+ }
399
+
572
400
  return createSchema({ ...base, primitive: specialPrimitive, type: specialType as ScalarSchemaType })
573
401
  }
574
402
 
575
403
  /**
576
404
  * 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
405
  */
585
- function convertEnum({ schema, name, nullable, type, options }: SchemaContext): SchemaNode {
586
- // Malformed schema: `{ type: 'array', enum: [...] }` — normalize by moving the enum into items.
406
+ function convertEnum({ schema, name, nullable, type, rawOptions }: SchemaContext): SchemaNode {
587
407
  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)
408
+ return parseSchema({ schema: normalizeArrayEnum(schema), name }, rawOptions)
592
409
  }
593
410
 
594
- // `null` in enum values is the OAS 3.0 convention for a nullable enum.
595
411
  const nullInEnum = schema.enum!.includes(null)
596
412
  const filteredValues = (nullInEnum ? schema.enum!.filter((v) => v !== null) : schema.enum!) as Array<string | number | boolean>
597
413
  const enumNullable = nullable || nullInEnum || undefined
@@ -612,56 +428,27 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
612
428
  example: schema.example,
613
429
  }
614
430
 
615
- // x-enumNames / x-enum-varnames: named variants with explicit labels take priority.
616
431
  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)
432
+ if (extensionKey || enumPrimitive === 'number' || enumPrimitive === 'integer' || enumPrimitive === 'boolean') {
433
+ const enumPrimitiveType = (enumPrimitive === 'number' || enumPrimitive === 'integer' ? 'number' : enumPrimitive === 'boolean' ? 'boolean' : 'string') as
434
+ | 'number'
435
+ | 'boolean'
436
+ | 'string'
437
+ const sourceValues = extensionKey
438
+ ? [...new Set((schema as Record<string, unknown>)[extensionKey] as Array<string | number>)]
439
+ : [...new Set(filteredValues)]
626
440
 
627
441
  return createSchema({
628
442
  ...enumBase,
629
- enumType,
630
- namedEnumValues: uniqueNames.map((label, index) => ({
443
+ primitive: enumPrimitiveType,
444
+ namedEnumValues: sourceValues.map((label, index) => ({
631
445
  name: String(label),
632
- value: filteredValues[index] ?? label,
633
- format: enumType,
634
- })),
635
- })
636
- }
637
-
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,
446
+ value: extensionKey ? (filteredValues[index] ?? label) : label,
447
+ primitive: enumPrimitiveType,
660
448
  })),
661
449
  })
662
450
  }
663
451
 
664
- // Plain string enum (default path).
665
452
  return createSchema({
666
453
  ...enumBase,
667
454
  enumValues: [...new Set(filteredValues)],
@@ -669,36 +456,32 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
669
456
  }
670
457
 
671
458
  /**
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`
459
+ * Converts an object-like schema into an `ObjectSchemaNode`.
682
460
  */
683
- function convertObject({ schema, name, nullable, defaultValue, options, mergedOptions }: SchemaContext): SchemaNode {
461
+ function convertObject({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): SchemaNode {
684
462
  const properties: Array<PropertyNode> = schema.properties
685
463
  ? Object.entries(schema.properties).map(([propName, propSchema]) => {
686
464
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required
687
465
  const resolvedPropSchema = propSchema as SchemaObject
688
466
  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
467
+
468
+ const resolvedChildName = childName(name, propName)
469
+ const propNode = parseSchema({ schema: resolvedPropSchema, name: resolvedChildName }, rawOptions)
470
+ let schemaNode = setEnumName(propNode, name, propName, options.enumSuffix)
471
+
472
+ const tupleNode = narrowSchema(schemaNode, 'tuple')
473
+ if (tupleNode?.items) {
474
+ const namedItems = tupleNode.items.map((item) => setEnumName(item, name, propName, options.enumSuffix))
475
+ if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
476
+ schemaNode = { ...tupleNode, items: namedItems }
477
+ }
478
+ }
694
479
 
695
480
  return createProperty({
696
481
  name: propName,
697
482
  schema: {
698
483
  ...schemaNode,
699
- nullable: propNullable || undefined,
700
- optional: !required && !propNullable ? true : undefined,
701
- nullish: !required && propNullable ? true : undefined,
484
+ nullable: schemaNode.type === 'null' ? undefined : propNullable || undefined,
702
485
  },
703
486
  required,
704
487
  })
@@ -706,15 +489,15 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
706
489
  : []
707
490
 
708
491
  const additionalProperties = schema.additionalProperties
709
- let additionalPropertiesNode: SchemaNode | true | undefined
492
+ let additionalPropertiesNode: SchemaNode | boolean | undefined
710
493
  if (additionalProperties === true) {
711
494
  additionalPropertiesNode = true
712
495
  } else if (additionalProperties && Object.keys(additionalProperties).length > 0) {
713
- additionalPropertiesNode = convertSchema({ schema: additionalProperties as SchemaObject }, options)
496
+ additionalPropertiesNode = parseSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
714
497
  } else if (additionalProperties === false) {
715
- additionalPropertiesNode = undefined
498
+ additionalPropertiesNode = false
716
499
  } else if (additionalProperties) {
717
- additionalPropertiesNode = createSchema({ type: resolveTypeOption(mergedOptions.unknownType) })
500
+ additionalPropertiesNode = createSchema({ type: typeOptionMap.get(options.unknownType)! })
718
501
  }
719
502
 
720
503
  const rawPatternProperties = 'patternProperties' in schema ? schema.patternProperties : undefined
@@ -724,8 +507,8 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
724
507
  Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [
725
508
  pattern,
726
509
  patternSchema === true || (typeof patternSchema === 'object' && Object.keys(patternSchema).length === 0)
727
- ? createSchema({ type: resolveTypeOption(mergedOptions.unknownType) })
728
- : convertSchema({ schema: patternSchema as SchemaObject }, options),
510
+ ? createSchema({ type: typeOptionMap.get(options.unknownType)! })
511
+ : parseSchema({ schema: patternSchema as SchemaObject }, rawOptions),
729
512
  ]),
730
513
  )
731
514
  : undefined
@@ -736,16 +519,16 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
736
519
  properties,
737
520
  additionalProperties: additionalPropertiesNode,
738
521
  patternProperties,
739
- ...buildSchemaBase(schema, name, nullable, defaultValue),
522
+ minProperties: schema.minProperties,
523
+ maxProperties: schema.maxProperties,
524
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
740
525
  })
741
526
 
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
527
  if (isDiscriminator(schema) && schema.discriminator.mapping) {
745
528
  const discPropName = schema.discriminator.propertyName
746
529
  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 })
530
+ const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : undefined
531
+ return setDiscriminatorEnum({ node: objectNode, propertyName: discPropName, values, enumName })
749
532
  }
750
533
 
751
534
  return objectNode
@@ -753,13 +536,10 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
753
536
 
754
537
  /**
755
538
  * 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
539
  */
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
540
+ function convertTuple({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): SchemaNode {
541
+ const tupleItems = (schema.prefixItems ?? []).map((item) => parseSchema({ schema: item as SchemaObject }, rawOptions))
542
+ const rest = schema.items ? parseSchema({ schema: schema.items as SchemaObject }, rawOptions) : createSchema({ type: 'any' })
763
543
 
764
544
  return createSchema({
765
545
  type: 'tuple',
@@ -768,22 +548,17 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
768
548
  rest,
769
549
  min: schema.minItems,
770
550
  max: schema.maxItems,
771
- ...buildSchemaBase(schema, name, nullable, defaultValue),
551
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
772
552
  })
773
553
  }
774
554
 
775
555
  /**
776
556
  * 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
557
  */
781
- function convertArray({ schema, name, nullable, defaultValue, options, mergedOptions }: SchemaContext): SchemaNode {
558
+ function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): SchemaNode {
782
559
  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)] : []
560
+ const itemName = rawItems?.enum?.length && name ? enumPropName(undefined, name, options.enumSuffix) : undefined
561
+ const items = rawItems ? [parseSchema({ schema: rawItems, name: itemName }, rawOptions)] : []
787
562
 
788
563
  return createSchema({
789
564
  type: 'array',
@@ -792,12 +567,12 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
792
567
  min: schema.minItems,
793
568
  max: schema.maxItems,
794
569
  unique: schema.uniqueItems ?? undefined,
795
- ...buildSchemaBase(schema, name, nullable, defaultValue),
570
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
796
571
  })
797
572
  }
798
573
 
799
574
  /**
800
- * Converts a `type: 'string'` schema (without a special format) into a `StringSchemaNode`.
575
+ * Converts a `type: 'string'` schema into a `StringSchemaNode`.
801
576
  */
802
577
  function convertString({ schema, name, nullable, defaultValue }: SchemaContext): SchemaNode {
803
578
  return createSchema({
@@ -806,12 +581,12 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
806
581
  min: schema.minLength,
807
582
  max: schema.maxLength,
808
583
  pattern: schema.pattern,
809
- ...buildSchemaBase(schema, name, nullable, defaultValue),
584
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
810
585
  })
811
586
  }
812
587
 
813
588
  /**
814
- * Converts a `type: 'number'` or `type: 'integer'` schema into the corresponding `SchemaNode`.
589
+ * Converts a `type: 'number'` or `type: 'integer'` schema.
815
590
  */
816
591
  function convertNumeric({ schema, name, nullable, defaultValue }: SchemaContext, type: 'number' | 'integer'): SchemaNode {
817
592
  return createSchema({
@@ -821,23 +596,24 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
821
596
  max: schema.maximum,
822
597
  exclusiveMinimum: typeof schema.exclusiveMinimum === 'number' ? schema.exclusiveMinimum : undefined,
823
598
  exclusiveMaximum: typeof schema.exclusiveMaximum === 'number' ? schema.exclusiveMaximum : undefined,
824
- ...buildSchemaBase(schema, name, nullable, defaultValue),
599
+ multipleOf: schema.multipleOf,
600
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
825
601
  })
826
602
  }
827
603
 
828
604
  /**
829
- * Converts a `type: 'boolean'` schema into a `BooleanSchemaNode`.
605
+ * Converts a `type: 'boolean'` schema.
830
606
  */
831
607
  function convertBoolean({ schema, name, nullable, defaultValue }: SchemaContext): SchemaNode {
832
608
  return createSchema({
833
609
  type: 'boolean',
834
610
  primitive: 'boolean',
835
- ...buildSchemaBase(schema, name, nullable, defaultValue),
611
+ ...buildSchemaNode(schema, name, nullable, defaultValue),
836
612
  })
837
613
  }
838
614
 
839
615
  /**
840
- * Converts an explicit `type: 'null'` or `const: null` schema into a `NullSchemaNode`.
616
+ * Converts an explicit `type: 'null'` schema.
841
617
  */
842
618
  function convertNull({ schema, name, nullable }: SchemaContext): SchemaNode {
843
619
  return createSchema({
@@ -852,67 +628,42 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
852
628
  }
853
629
 
854
630
  /**
855
- * Central dispatcher: converts an OAS `SchemaObject` into a `SchemaNode`.
631
+ * Central dispatcher that converts an OAS `SchemaObject` into a `SchemaNode`.
856
632
  *
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)
633
+ * Dispatch order (first match wins): `$ref` → `allOf` → `oneOf`/`anyOf` → `const` → `format`
634
+ * octet-stream blob → multi-type array → constraint-inferred type → `enum` → object/array/tuple/scalar
635
+ * empty-schema fallback (`emptySchemaType` option).
869
636
  */
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.
637
+ function parseSchema({ schema, name }: { schema: SchemaObject; name?: string | null }, rawOptions?: Partial<ParserOptions>): SchemaNode {
638
+ const options: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...rawOptions }
874
639
  const flattenedSchema = flattenSchema(schema)
875
640
  if (flattenedSchema && flattenedSchema !== schema) {
876
- return convertSchema({ schema: flattenedSchema, name }, options)
641
+ return parseSchema({ schema: flattenedSchema, name }, rawOptions)
877
642
  }
878
643
 
879
644
  const nullable = isNullable(schema) || undefined
880
645
  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
646
  const type = Array.isArray(schema.type) ? schema.type[0] : schema.type
883
647
 
884
- const ctx: SchemaContext = { schema, name, nullable, defaultValue, type, options, mergedOptions }
648
+ const ctx: SchemaContext = { schema, name, nullable, defaultValue, type, rawOptions, options }
885
649
 
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
650
  if (isReference(schema)) return convertRef(ctx)
890
651
 
891
- // Composition keywords
892
652
  if (schema.allOf?.length) return convertAllOf(ctx)
893
653
  const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
894
654
  if (unionMembers.length) return convertUnion(ctx)
895
655
 
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
656
  if ('const' in schema && schema.const !== undefined) return convertConst(ctx)
899
657
 
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
658
  if (schema.format) {
904
659
  const formatResult = convertFormat(ctx)
905
660
  if (formatResult) return formatResult
906
661
  }
907
662
 
908
- // OAS 3.1: `contentMediaType: 'application/octet-stream'` on a string schema signals binary data.
909
663
  if (schema.type === 'string' && schema.contentMediaType === 'application/octet-stream') {
910
- return createSchema({ type: 'blob', primitive: 'string', ...buildSchemaBase(schema, name, nullable, defaultValue) })
664
+ return createSchema({ type: 'blob', primitive: 'string', ...buildSchemaNode(schema, name, nullable, defaultValue) })
911
665
  }
912
666
 
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
667
  if (Array.isArray(schema.type) && schema.type.length > 1) {
917
668
  const nonNullTypes = schema.type.filter((t) => t !== 'null') as string[]
918
669
  const arrayNullable = schema.type.includes('null') || nullable || undefined
@@ -920,15 +671,12 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
920
671
  if (nonNullTypes.length > 1) {
921
672
  return createSchema({
922
673
  type: 'union',
923
- members: nonNullTypes.map((t) => convertSchema({ schema: { ...schema, type: t } as SchemaObject, name }, options)),
924
- ...buildSchemaBase(schema, name, arrayNullable, defaultValue),
674
+ members: nonNullTypes.map((t) => parseSchema({ schema: { ...schema, type: t } as SchemaObject, name }, rawOptions)),
675
+ ...buildSchemaNode(schema, name, arrayNullable, defaultValue),
925
676
  })
926
677
  }
927
678
  }
928
679
 
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
680
  if (!type) {
933
681
  if (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined) {
934
682
  return convertString(ctx)
@@ -948,52 +696,84 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
948
696
  if (type === 'boolean') return convertBoolean(ctx)
949
697
  if (type === 'null') return convertNull(ctx)
950
698
 
951
- const emptyType = resolveTypeOption(mergedOptions.emptySchemaType)
699
+ const emptyType = typeOptionMap.get(options.emptySchemaType)!
952
700
  return createSchema({ type: emptyType as ScalarSchemaType, name, title: schema.title, description: schema.description })
953
701
  }
954
702
 
955
703
  /**
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`.
704
+ * Converts a dereferenced OAS parameter object into a `ParameterNode`.
958
705
  */
959
- function parseParameter(options: Options, param: Record<string, unknown>): ParameterNode {
706
+ function parseParameter(options: ParserOptions, param: Record<string, unknown>): ParameterNode {
960
707
  const required = (param['required'] as boolean | undefined) ?? false
961
708
 
962
- const schema: SchemaNode =
963
- param['schema'] && !isReference(param['schema'])
964
- ? convertSchema({ schema: param['schema'] as SchemaObject }, options)
965
- : createSchema({ type: resolveTypeOption(options.unknownType) })
709
+ const schema: SchemaNode = param['schema']
710
+ ? parseSchema({ schema: param['schema'] as SchemaObject }, options)
711
+ : createSchema({ type: typeOptionMap.get(options.unknownType)! })
966
712
 
967
713
  return createParameter({
968
714
  name: param['name'] as string,
969
715
  in: param['in'] as ParameterLocation,
970
716
  schema: {
971
717
  ...schema,
972
- optional: (!required || !!schema.optional) ? true : undefined,
718
+ description: (param['description'] as string | undefined) ?? schema.description,
973
719
  },
974
720
  required,
975
721
  })
976
722
  }
977
723
 
978
724
  /**
979
- * Converts an OAS `Operation` into an `OperationNode`, resolving parameters,
980
- * request body, and all response codes into their AST node equivalents.
725
+ * Converts an OAS `Operation` into an `OperationNode`.
981
726
  */
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>
727
+ function parseOperation(options: ParserOptions, operation: Operation): OperationNode {
728
+ const parameters: Array<ParameterNode> = getParameters(document, operation).map((param) =>
729
+ parseParameter(options, param as unknown as Record<string, unknown>),
730
+ )
985
731
 
986
- return parseParameter(options, dereferenced)
987
- })
732
+ const requestBodySchema = getRequestSchema(document, operation, { contentType: ctx.contentType })
733
+ const requestBodySchemaNode = requestBodySchema ? parseSchema({ schema: requestBodySchema }, options) : undefined
734
+
735
+ const requestBodyDescription =
736
+ operation.schema.requestBody && !isReference(operation.schema.requestBody)
737
+ ? (operation.schema.requestBody as { description?: string }).description
738
+ : undefined
739
+
740
+ const requestBodyKeysToOmit = requestBodySchema?.properties
741
+ ? Object.entries(requestBodySchema.properties)
742
+ .filter(([, prop]) => !isReference(prop) && (prop as { readOnly?: boolean }).readOnly)
743
+ .map(([key]) => key)
744
+ : undefined
745
+
746
+ const requestBodyRequired =
747
+ operation.schema.requestBody && !isReference(operation.schema.requestBody)
748
+ ? (operation.schema.requestBody as { required?: boolean }).required === true
749
+ : false
988
750
 
989
- const requestBodySchema = oas.getRequestSchema(operation)
990
- const requestBody = requestBodySchema ? convertSchema({ schema: requestBodySchema }, options) : undefined
751
+ const requestBodyContentType = (() => {
752
+ if (!operation.schema.requestBody || isReference(operation.schema.requestBody)) {
753
+ return undefined
754
+ }
755
+ const content = (operation.schema.requestBody as { content?: Record<string, unknown> }).content
756
+ return content ? Object.keys(content)[0] : undefined
757
+ })()
758
+
759
+ const requestBody = requestBodySchemaNode
760
+ ? {
761
+ description: requestBodyDescription,
762
+ schema: syncOptionality(requestBodySchemaNode, requestBodyRequired),
763
+ keysToOmit: requestBodyKeysToOmit?.length ? requestBodyKeysToOmit : undefined,
764
+ required: requestBodyRequired || undefined,
765
+ contentType: requestBodyContentType,
766
+ }
767
+ : undefined
991
768
 
992
769
  const responses: Array<ResponseNode> = operation.getResponseStatusCodes().map((statusCode) => {
993
770
  const responseObj = operation.getResponseByStatusCode(statusCode)
994
- const responseSchema = oas.getResponseSchema(operation, statusCode)
771
+ const responseSchema = getResponseSchema(document, operation, statusCode, { contentType: ctx.contentType })
995
772
 
996
- const schema = responseSchema && Object.keys(responseSchema).length > 0 ? convertSchema({ schema: responseSchema }, options) : undefined
773
+ const schema =
774
+ responseSchema && Object.keys(responseSchema).length > 0
775
+ ? parseSchema({ schema: responseSchema }, options)
776
+ : createSchema({ type: typeOptionMap.get(options.emptySchemaType)! })
997
777
 
998
778
  const description = typeof responseObj === 'object' && responseObj !== null && !Array.isArray(responseObj) ? responseObj.description : undefined
999
779
 
@@ -1002,20 +782,29 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
1002
782
  ? (responseObj as { content?: Record<string, unknown> }).content
1003
783
  : undefined
1004
784
 
1005
- const mediaType = rawContent ? toMediaType(Object.keys(rawContent)[0] ?? '') : toMediaType(operation.contentType ?? '')
785
+ const mediaType = rawContent ? getMediaType(Object.keys(rawContent)[0] ?? '') : getMediaType(operation.contentType ?? '')
786
+
787
+ const keysToOmit = responseSchema?.properties
788
+ ? Object.entries(responseSchema.properties)
789
+ .filter(([, prop]) => !isReference(prop) && (prop as { writeOnly?: boolean }).writeOnly)
790
+ .map(([key]) => key)
791
+ : undefined
1006
792
 
1007
793
  return createResponse({
1008
794
  statusCode: statusCode as StatusCode,
1009
795
  description,
1010
796
  schema,
1011
797
  mediaType,
798
+ keysToOmit: keysToOmit?.length ? keysToOmit : undefined,
1012
799
  })
1013
800
  })
1014
801
 
802
+ const urlPath = new URLPath(operation.path)
803
+
1015
804
  return createOperation({
1016
805
  operationId: operation.getOperationId(),
1017
806
  method: operation.method.toUpperCase() as HttpMethod,
1018
- path: new URLPath(operation.path).URL,
807
+ path: urlPath.path,
1019
808
  tags: operation.getTags().map((tag) => tag.name),
1020
809
  summary: operation.getSummary() || undefined,
1021
810
  description: operation.getDescription() || undefined,
@@ -1026,63 +815,57 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
1026
815
  })
1027
816
  }
1028
817
 
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 }
818
+ return { parseSchema, parseOperation, parseParameter }
819
+ }
1035
820
 
1036
- const schemas: Array<SchemaNode> = Object.entries(schemaObjects).map(([name, schemaObject]) =>
1037
- convertSchema({ schema: schemaObject as SchemaObject, name }, mergedOptions),
1038
- )
821
+ /**
822
+ * Converts a single `SchemaObject` into a `SchemaNode`.
823
+ *
824
+ * @example
825
+ * ```ts
826
+ * const ctx = { document }
827
+ * parseSchema(ctx, { schema: { type: 'string', format: 'uuid' } })
828
+ * ```
829
+ */
830
+ export function parseSchema(ctx: OasParserContext, { schema, name }: { schema: SchemaObject; name?: string }, options?: Partial<ParserOptions>): SchemaNode {
831
+ return createSchemaParser(ctx).parseSchema({ schema, name }, options)
832
+ }
1039
833
 
1040
- const paths = oas.getPaths()
834
+ /**
835
+ * Converts the entire OpenAPI spec into a `RootNode` (the top-level `@kubb/ast` tree).
836
+ *
837
+ * This is the main entry point: `OpenAPI / Swagger → Kubb AST`.
838
+ * No code is generated here — the resulting tree is spec-agnostic and consumed by
839
+ * downstream plugins (`plugin-ts`, `plugin-zod`, …).
840
+ *
841
+ * @example
842
+ * ```ts
843
+ * const document = await parseFromConfig(config)
844
+ * const root = parseOas(document, { dateType: 'date', contentType: 'application/json' })
845
+ * ```
846
+ */
847
+ export function parseOas(
848
+ document: Document,
849
+ options: Partial<ParserOptions> & { contentType?: ContentType } = {},
850
+ ): { root: RootNode; nameMapping: Map<string, string> } {
851
+ const { contentType, ...parserOptions } = options
852
+ const mergedOptions: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...parserOptions }
1041
853
 
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
- )
854
+ const { schemas: schemaObjects, nameMapping } = getSchemas(document, { contentType })
855
+ const { parseSchema: _parseSchema, parseOperation: _parseOperation } = createSchemaParser({ document, contentType })
1047
856
 
1048
- return createRoot({ schemas, operations })
1049
- }
857
+ const schemas: Array<SchemaNode> = Object.entries(schemaObjects).map(([name, schema]) => _parseSchema({ schema, name }, mergedOptions))
1050
858
 
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
- }
859
+ const baseOas = new BaseOas(document)
860
+ const paths = baseOas.getPaths()
1071
861
 
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
- }
862
+ const operations: Array<OperationNode> = Object.entries(paths).flatMap(([_path, methods]) =>
863
+ Object.entries(methods)
864
+ .map(([, operation]) => (operation ? _parseOperation(mergedOptions, operation) : null))
865
+ .filter((op): op is OperationNode => op !== null),
866
+ )
867
+
868
+ const root = createRoot({ schemas, operations })
1081
869
 
1082
- return {
1083
- parse,
1084
- convertSchema,
1085
- resolveRefs,
1086
- nameMapping,
1087
- } as OasParser
870
+ return { root, nameMapping }
1088
871
  }