diff --git a/packages/shared/src/openApi/shared/transforms/readWrite.ts b/packages/shared/src/openApi/shared/transforms/readWrite.ts index 114587fa3..9dc46d478 100644 --- a/packages/shared/src/openApi/shared/transforms/readWrite.ts +++ b/packages/shared/src/openApi/shared/transforms/readWrite.ts @@ -437,6 +437,76 @@ export const splitSchemas = ({ split.reverseMapping[writePointer] = pointer; } + // Helper function to update discriminator mappings in a schema + const updateDiscriminatorMappingsInSchema = ( + schema: unknown, + contextVariant: 'read' | 'write', + ) => { + if (schema && typeof schema === 'object') { + // If this schema has a discriminator with a mapping + if ( + 'discriminator' in schema && + schema.discriminator && + typeof schema.discriminator === 'object' && + 'mapping' in schema.discriminator && + schema.discriminator.mapping && + typeof schema.discriminator.mapping === 'object' + ) { + const mapping = schema.discriminator.mapping as Record; + const updatedMapping: Record = {}; + + for (const [discriminatorValue, originalRef] of Object.entries(mapping)) { + const map = split.mapping[originalRef]; + if (map) { + // Update to the appropriate variant + if (contextVariant === 'read' && map.read) { + updatedMapping[discriminatorValue] = map.read; + } else if (contextVariant === 'write' && map.write) { + updatedMapping[discriminatorValue] = map.write; + } else { + updatedMapping[discriminatorValue] = originalRef; + } + } else { + updatedMapping[discriminatorValue] = originalRef; + } + } + + (schema.discriminator as Record).mapping = updatedMapping; + } + + // Recursively update discriminators in allOf, oneOf, anyOf + for (const key of ['allOf', 'oneOf', 'anyOf'] as const) { + if (key in schema && Array.isArray((schema as Record)[key])) { + const compositions = (schema as Record)[key] as Array; + for (const comp of compositions) { + updateDiscriminatorMappingsInSchema(comp, contextVariant); + } + } + } + } + }; + + // Update discriminator mappings in all split schemas + for (const [name, schema] of Object.entries(split.schemas)) { + const pointer = `${schemasPointerNamespace}${name}`; + const originalPointer = split.reverseMapping[pointer]; + + if (originalPointer) { + const mapping = split.mapping[originalPointer]; + if (mapping) { + // Determine if this is a read or write variant + const isRead = mapping.read === pointer; + const isWrite = mapping.write === pointer; + + if (isRead) { + updateDiscriminatorMappingsInSchema(schema, 'read'); + } else if (isWrite) { + updateDiscriminatorMappingsInSchema(schema, 'write'); + } + } + } + } + event.timeEnd(); return split; }; @@ -630,6 +700,39 @@ export const updateRefsInSpec = ({ (node as Record)[key] = map.read; } } + } else if (key === 'discriminator' && typeof value === 'object' && value !== null) { + // Update discriminator mappings to point to the correct read/write variants + const discriminator = value as Record; + if (discriminator.mapping && typeof discriminator.mapping === 'object') { + const mapping = discriminator.mapping as Record; + const updatedMapping: Record = {}; + for (const [discriminatorValue, originalRef] of Object.entries(mapping)) { + const map = split.mapping[originalRef]; + if (map) { + if (nextContext === 'read' && map.read) { + updatedMapping[discriminatorValue] = map.read; + } else if (nextContext === 'write' && map.write) { + updatedMapping[discriminatorValue] = map.write; + } else { + // For schemas with no context, don't update the mapping. + // This preserves the original mapping for base schemas. + updatedMapping[discriminatorValue] = originalRef; + } + } else { + updatedMapping[discriminatorValue] = originalRef; + } + } + discriminator.mapping = updatedMapping; + } + // Continue walking the discriminator object for other properties + walk({ + context: nextContext, + currentPointer: nextPointer, + inSchema, + node: value, + path: [...path, key], + visited, + }); } else { walk({ context: nextContext, diff --git a/specs/3.1.x/discriminator-one-of-read-write.yaml b/specs/3.1.x/discriminator-one-of-read-write.yaml new file mode 100644 index 000000000..e5aa0af31 --- /dev/null +++ b/specs/3.1.x/discriminator-one-of-read-write.yaml @@ -0,0 +1,107 @@ +openapi: 3.1.0 +info: + title: OpenAPI 3.1.0 discriminator one of with read/write example + version: 1 +paths: + /pets: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreatePetRequest' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CreatePetResponse' + description: OK +components: + schemas: + AnimalPayload: + type: object + required: + - typeDiscriminator + properties: + typeDiscriminator: + type: string + discriminator: + propertyName: typeDiscriminator + mapping: + dog: '#/components/schemas/DogPayload' + cat: '#/components/schemas/CatPayload' + + DogPayload: + allOf: + - $ref: '#/components/schemas/AnimalPayload' + - type: object + required: + - breed + - canFetch + - displayBreed + properties: + breed: + type: string + canFetch: + type: boolean + displayBreed: + type: string + readOnly: true + + CatPayload: + allOf: + - $ref: '#/components/schemas/AnimalPayload' + - type: object + required: + - breed + - livesRemaining + - displayBreed + properties: + breed: + type: string + livesRemaining: + type: integer + displayBreed: + type: string + readOnly: true + + CreatePetRequest: + type: object + required: + - name + - animal + properties: + name: + type: string + animal: + oneOf: + - $ref: '#/components/schemas/DogPayload' + - $ref: '#/components/schemas/CatPayload' + discriminator: + propertyName: typeDiscriminator + mapping: + dog: '#/components/schemas/DogPayload' + cat: '#/components/schemas/CatPayload' + + CreatePetResponse: + type: object + required: + - id + - name + - animal + properties: + id: + type: string + name: + type: string + animal: + oneOf: + - $ref: '#/components/schemas/DogPayload' + - $ref: '#/components/schemas/CatPayload' + discriminator: + propertyName: typeDiscriminator + mapping: + dog: '#/components/schemas/DogPayload' + cat: '#/components/schemas/CatPayload' -- 2.51.2 From 0b6a4fd3f3bcbca269a680ae0f1ca0944ac24959 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 21:48:56 +0000 Subject: [PATCH 3/6] Add snapshot test for discriminator with oneOf and read/write transforms Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .../openapi-ts-tests/main/test/3.1.x.test.ts | 7 ++ .../discriminator-one-of-read-write/index.ts | 3 + .../types.gen.ts | 89 +++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-one-of-read-write/index.ts create mode 100644 packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-one-of-read-write/types.gen.ts diff --git a/packages/openapi-ts-tests/main/test/3.1.x.test.ts b/packages/openapi-ts-tests/main/test/3.1.x.test.ts index 9a916c31b..b7421698f 100644 --- a/packages/openapi-ts-tests/main/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/main/test/3.1.x.test.ts @@ -227,6 +227,13 @@ describe(`OpenAPI ${version}`, () => { }), description: 'handles nested allOf with discriminators', }, + { + config: createConfig({ + input: 'discriminator-one-of-read-write.yaml', + output: 'discriminator-one-of-read-write', + }), + description: 'handles discriminator with oneOf and read/write transforms', + }, { config: createConfig({ input: 'duplicate-null.json', diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-one-of-read-write/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-one-of-read-write/index.ts new file mode 100644 index 000000000..517836f3a --- /dev/null +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-one-of-read-write/index.ts @@ -0,0 +1,3 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { AnimalPayload, CatPayload, CatPayloadWritable, ClientOptions, CreatePetRequest, CreatePetRequestWritable, CreatePetResponse, CreatePetResponseWritable, DogPayload, DogPayloadWritable, PostPetsData, PostPetsResponse, PostPetsResponses } from './types.gen'; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-one-of-read-write/types.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-one-of-read-write/types.gen.ts new file mode 100644 index 000000000..4bd42da75 --- /dev/null +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-one-of-read-write/types.gen.ts @@ -0,0 +1,89 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}); +}; + +export type AnimalPayload = { + typeDiscriminator: string; +}; + +export type DogPayload = Omit & { + breed: string; + canFetch: boolean; + readonly displayBreed: string; + typeDiscriminator: 'dog'; +}; + +export type CatPayload = Omit & { + breed: string; + livesRemaining: number; + readonly displayBreed: string; + typeDiscriminator: 'cat'; +}; + +export type CreatePetRequest = { + name: string; + animal: ({ + typeDiscriminator: 'dog'; + } & DogPayload) | ({ + typeDiscriminator: 'cat'; + } & CatPayload); +}; + +export type CreatePetResponse = { + id: string; + name: string; + animal: ({ + typeDiscriminator: 'dog'; + } & DogPayload) | ({ + typeDiscriminator: 'cat'; + } & CatPayload); +}; + +export type DogPayloadWritable = Omit & { + breed: string; + canFetch: boolean; + typeDiscriminator: 'DogPayloadWritable'; +}; + +export type CatPayloadWritable = Omit & { + breed: string; + livesRemaining: number; + typeDiscriminator: 'CatPayloadWritable'; +}; + +export type CreatePetRequestWritable = { + name: string; + animal: ({ + typeDiscriminator: 'dog'; + } & DogPayloadWritable) | ({ + typeDiscriminator: 'cat'; + } & CatPayloadWritable); +}; + +export type CreatePetResponseWritable = { + id: string; + name: string; + animal: ({ + typeDiscriminator: 'dog'; + } & DogPayloadWritable) | ({ + typeDiscriminator: 'cat'; + } & CatPayloadWritable); +}; + +export type PostPetsData = { + body: CreatePetRequestWritable; + path?: never; + query?: never; + url: '/pets'; +}; + +export type PostPetsResponses = { + /** + * OK + */ + 200: CreatePetResponse; +}; + +export type PostPetsResponse = PostPetsResponses[keyof PostPetsResponses]; -- 2.51.2 From 8126ad1b2ebff23daaf574d2b62bf4d7ee3ee511 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:27:23 +0000 Subject: [PATCH 4/6] Fix discriminator values in writable type variants Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .../discriminator-one-of-read-write/index.ts | 2 +- .../types.gen.ts | 12 +- .../openApi/shared/transforms/readWrite.ts | 180 ++++++++++++++---- 3 files changed, 153 insertions(+), 41 deletions(-) diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-one-of-read-write/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-one-of-read-write/index.ts index 517836f3a..461d5fce9 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-one-of-read-write/index.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-one-of-read-write/index.ts @@ -1,3 +1,3 @@ // This file is auto-generated by @hey-api/openapi-ts -export type { AnimalPayload, CatPayload, CatPayloadWritable, ClientOptions, CreatePetRequest, CreatePetRequestWritable, CreatePetResponse, CreatePetResponseWritable, DogPayload, DogPayloadWritable, PostPetsData, PostPetsResponse, PostPetsResponses } from './types.gen'; +export type { AnimalPayload, AnimalPayloadWritable, CatPayload, CatPayloadWritable, ClientOptions, CreatePetRequest, CreatePetRequestWritable, CreatePetResponse, CreatePetResponseWritable, DogPayload, DogPayloadWritable, PostPetsData, PostPetsResponse, PostPetsResponses } from './types.gen'; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-one-of-read-write/types.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-one-of-read-write/types.gen.ts index 4bd42da75..980f8df91 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-one-of-read-write/types.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-one-of-read-write/types.gen.ts @@ -41,16 +41,16 @@ export type CreatePetResponse = { } & CatPayload); }; -export type DogPayloadWritable = Omit & { +export type DogPayloadWritable = Omit & { breed: string; canFetch: boolean; - typeDiscriminator: 'DogPayloadWritable'; + typeDiscriminator: 'dog'; }; -export type CatPayloadWritable = Omit & { +export type CatPayloadWritable = Omit & { breed: string; livesRemaining: number; - typeDiscriminator: 'CatPayloadWritable'; + typeDiscriminator: 'cat'; }; export type CreatePetRequestWritable = { @@ -72,6 +72,10 @@ export type CreatePetResponseWritable = { } & CatPayloadWritable); }; +export type AnimalPayloadWritable = { + typeDiscriminator: string; +}; + export type PostPetsData = { body: CreatePetRequestWritable; path?: never; diff --git a/packages/shared/src/openApi/shared/transforms/readWrite.ts b/packages/shared/src/openApi/shared/transforms/readWrite.ts index 9dc46d478..08f20db40 100644 --- a/packages/shared/src/openApi/shared/transforms/readWrite.ts +++ b/packages/shared/src/openApi/shared/transforms/readWrite.ts @@ -437,31 +437,108 @@ export const splitSchemas = ({ split.reverseMapping[writePointer] = pointer; } - // Helper function to update discriminator mappings in a schema - const updateDiscriminatorMappingsInSchema = ( - schema: unknown, - contextVariant: 'read' | 'write', - ) => { - if (schema && typeof schema === 'object') { - // If this schema has a discriminator with a mapping + //Create writable variants of parent schemas that have discriminators and are referenced by split schemas + const parentSchemasToSplit = new Map>(); + + // First pass: identify parent schemas that need writable variants + for (const [name, schema] of Object.entries(split.schemas)) { + const pointer = `${schemasPointerNamespace}${name}`; + const originalPointer = split.reverseMapping[pointer]; + + if (originalPointer) { + const mapping = split.mapping[originalPointer]; + if (mapping) { + const contextVariant = + mapping.read === pointer ? 'read' : mapping.write === pointer ? 'write' : null; + + if (contextVariant && schema && typeof schema === 'object') { + // Check allOf for $refs to schemas with discriminators + if ('allOf' in schema && Array.isArray((schema as Record).allOf)) { + const allOf = (schema as Record).allOf as Array; + + for (const comp of allOf) { + if ( + comp && + typeof comp === 'object' && + '$ref' in comp && + typeof comp.$ref === 'string' + ) { + const ref = comp.$ref as string; + const schemasObj = getSchemasObject(spec); + + if (schemasObj) { + const refPath = jsonPointerToPath(ref); + const schemaName = refPath[refPath.length - 1]; + + if (typeof schemaName === 'string' && schemaName in schemasObj) { + const resolvedSchema = schemasObj[schemaName]; + + // Check if this schema has a discriminator with mapping + if ( + resolvedSchema && + typeof resolvedSchema === 'object' && + 'discriminator' in resolvedSchema && + resolvedSchema.discriminator && + typeof resolvedSchema.discriminator === 'object' && + 'mapping' in resolvedSchema.discriminator && + resolvedSchema.discriminator.mapping && + typeof resolvedSchema.discriminator.mapping === 'object' + ) { + // This parent schema needs a variant for this context + if (!parentSchemasToSplit.has(ref)) { + parentSchemasToSplit.set(ref, new Set()); + } + parentSchemasToSplit.get(ref)!.add(contextVariant); + } + } + } + } + } + } + } + } + } + } + + // Second pass: create writable variants of parent schemas and update their discriminator mappings + const parentSchemaVariants = new Map(); + + for (const [parentRef, contexts] of parentSchemasToSplit) { + const schemasObj = getSchemasObject(spec); + if (!schemasObj) continue; + + const refPath = jsonPointerToPath(parentRef); + const parentName = refPath[refPath.length - 1]; + + if (typeof parentName !== 'string' || !(parentName in schemasObj)) continue; + + const parentSchema = schemasObj[parentName]; + if (!parentSchema || typeof parentSchema !== 'object') continue; + + const variants: { read?: string; write?: string } = {}; + + // Create variants for each context + for (const context of contexts) { + const variantSchema = deepClone(parentSchema); + + // Update discriminator mapping in the variant if ( - 'discriminator' in schema && - schema.discriminator && - typeof schema.discriminator === 'object' && - 'mapping' in schema.discriminator && - schema.discriminator.mapping && - typeof schema.discriminator.mapping === 'object' + 'discriminator' in variantSchema && + variantSchema.discriminator && + typeof variantSchema.discriminator === 'object' && + 'mapping' in variantSchema.discriminator && + variantSchema.discriminator.mapping && + typeof variantSchema.discriminator.mapping === 'object' ) { - const mapping = schema.discriminator.mapping as Record; + const mapping = (variantSchema.discriminator as any).mapping as Record; const updatedMapping: Record = {}; for (const [discriminatorValue, originalRef] of Object.entries(mapping)) { const map = split.mapping[originalRef]; if (map) { - // Update to the appropriate variant - if (contextVariant === 'read' && map.read) { + if (context === 'read' && map.read) { updatedMapping[discriminatorValue] = map.read; - } else if (contextVariant === 'write' && map.write) { + } else if (context === 'write' && map.write) { updatedMapping[discriminatorValue] = map.write; } else { updatedMapping[discriminatorValue] = originalRef; @@ -471,22 +548,31 @@ export const splitSchemas = ({ } } - (schema.discriminator as Record).mapping = updatedMapping; + (variantSchema as any).discriminator.mapping = updatedMapping; } - // Recursively update discriminators in allOf, oneOf, anyOf - for (const key of ['allOf', 'oneOf', 'anyOf'] as const) { - if (key in schema && Array.isArray((schema as Record)[key])) { - const compositions = (schema as Record)[key] as Array; - for (const comp of compositions) { - updateDiscriminatorMappingsInSchema(comp, contextVariant); - } - } + // Add the variant to split.schemas with an appropriate name + if (context === 'write') { + const variantBase = applyNaming(parentName, config.requests); + const variantName = + variantBase === parentName + ? `${parentName}Writable` + : getUniqueComponentName({ + base: variantBase, + components: existingNames, + }); + existingNames.add(variantName); + split.schemas[variantName] = variantSchema; + variants.write = `${schemasPointerNamespace}${variantName}`; } + // We could create read variants too, but typically they're not needed + // since the original schema serves as the read variant } - }; - // Update discriminator mappings in all split schemas + parentSchemaVariants.set(parentRef, variants); + } + + // Third pass: update $refs in split schemas to point to the parent variants for (const [name, schema] of Object.entries(split.schemas)) { const pointer = `${schemasPointerNamespace}${name}`; const originalPointer = split.reverseMapping[pointer]; @@ -494,14 +580,36 @@ export const splitSchemas = ({ if (originalPointer) { const mapping = split.mapping[originalPointer]; if (mapping) { - // Determine if this is a read or write variant - const isRead = mapping.read === pointer; - const isWrite = mapping.write === pointer; - - if (isRead) { - updateDiscriminatorMappingsInSchema(schema, 'read'); - } else if (isWrite) { - updateDiscriminatorMappingsInSchema(schema, 'write'); + const contextVariant = + mapping.read === pointer ? 'read' : mapping.write === pointer ? 'write' : null; + + if (contextVariant && schema && typeof schema === 'object') { + // Update $refs in allOf + if ('allOf' in schema && Array.isArray((schema as Record).allOf)) { + const allOf = (schema as Record).allOf as Array; + + for (let i = 0; i < allOf.length; i++) { + const comp = allOf[i]; + + if ( + comp && + typeof comp === 'object' && + '$ref' in comp && + typeof comp.$ref === 'string' + ) { + const ref = comp.$ref as string; + const variants = parentSchemaVariants.get(ref); + + if (variants) { + if (contextVariant === 'write' && variants.write) { + (comp as any).$ref = variants.write; + } else if (contextVariant === 'read' && variants.read) { + (comp as any).$ref = variants.read; + } + } + } + } + } } } } -- 2.51.2 From 47dd2a31b976871696a42368c0f4ed65cceacecf Mon Sep 17 00:00:00 2001 From: Lubos Date: Wed, 4 Feb 2026 07:18:18 +0800 Subject: [PATCH 5/6] docs: add changelog --- .changeset/quiet-hats-switch.md | 5 + .../openApi/shared/transforms/readWrite.ts | 380 +++++++++--------- 2 files changed, 201 insertions(+), 184 deletions(-) create mode 100644 .changeset/quiet-hats-switch.md diff --git a/.changeset/quiet-hats-switch.md b/.changeset/quiet-hats-switch.md new file mode 100644 index 000000000..ca5e1239f --- /dev/null +++ b/.changeset/quiet-hats-switch.md @@ -0,0 +1,5 @@ +--- +"@hey-api/shared": patch +--- + +**transform(read-write)**: improve discriminated schemas split diff --git a/packages/shared/src/openApi/shared/transforms/readWrite.ts b/packages/shared/src/openApi/shared/transforms/readWrite.ts index 08f20db40..496a31bea 100644 --- a/packages/shared/src/openApi/shared/transforms/readWrite.ts +++ b/packages/shared/src/openApi/shared/transforms/readWrite.ts @@ -17,15 +17,17 @@ import { getUniqueComponentName, isPathRootSchema, specToSchemasPointerNamespace type OriginalSchemas = Record; +type SplitMapping = Record< + string, + { + read?: string; + write?: string; + } +>; + type SplitSchemas = { /** Key is the original schema pointer. */ - mapping: Record< - string, - { - read?: string; - write?: string; - } - >; + mapping: SplitMapping; /** splitPointer -> originalPointer */ reverseMapping: Record; /** name -> schema object */ @@ -312,6 +314,186 @@ const removeOriginalSplitSchemas = ({ event.timeEnd(); }; +/** + * Create writable variants of parent schemas that have discriminators + * and are referenced by split schemas. + */ +function splitDiscriminatorSchemas({ + config, + existingNames, + schemasPointerNamespace, + spec, + split, +}: { + config: ReadWriteConfig; + existingNames: Set; + schemasPointerNamespace: string; + spec: unknown; + split: SplitSchemas; +}) { + const schemasObj = getSchemasObject(spec); + if (!schemasObj) return; + + const parentSchemasToSplit = new Map>(); + + // First pass: identify parent schemas that need writable variants + for (const [name, schema] of Object.entries(split.schemas)) { + const pointer = `${schemasPointerNamespace}${name}`; + const originalPointer = split.reverseMapping[pointer]; + + if (originalPointer) { + const mapping = split.mapping[originalPointer]; + if (mapping) { + const contextVariant: Scope | null = + mapping.read === pointer ? 'read' : mapping.write === pointer ? 'write' : null; + + // Check allOf for $refs to schemas with discriminators + if ( + contextVariant && + schema && + typeof schema === 'object' && + 'allOf' in schema && + schema.allOf instanceof Array + ) { + for (const comp of schema.allOf) { + if ( + comp && + typeof comp === 'object' && + '$ref' in comp && + typeof comp.$ref === 'string' + ) { + const refPath = jsonPointerToPath(comp.$ref); + const schemaName = refPath[refPath.length - 1]; + + if (typeof schemaName === 'string' && schemaName in schemasObj) { + const resolvedSchema = schemasObj[schemaName]; + + // Check if this schema has a discriminator with mapping + if ( + resolvedSchema && + typeof resolvedSchema === 'object' && + 'discriminator' in resolvedSchema && + resolvedSchema.discriminator && + typeof resolvedSchema.discriminator === 'object' && + 'mapping' in resolvedSchema.discriminator && + resolvedSchema.discriminator.mapping && + typeof resolvedSchema.discriminator.mapping === 'object' + ) { + // This parent schema needs a variant for this context + if (!parentSchemasToSplit.has(comp.$ref)) { + parentSchemasToSplit.set(comp.$ref, new Set()); + } + parentSchemasToSplit.get(comp.$ref)!.add(contextVariant); + } + } + } + } + } + } + } + } + + // Second pass: create writable variants of parent schemas and update their discriminator mappings + const parentSchemaVariants = new Map(); + + for (const [parentRef, contexts] of parentSchemasToSplit) { + const refPath = jsonPointerToPath(parentRef); + const parentName = refPath[refPath.length - 1]; + + if (typeof parentName !== 'string' || !(parentName in schemasObj)) continue; + + const parentSchema = schemasObj[parentName]; + if (!parentSchema || typeof parentSchema !== 'object') continue; + + const variants: SplitMapping[keyof SplitMapping] = {}; + + // Create variants for each context + for (const context of contexts) { + const variantSchema = deepClone(parentSchema); + + // Update discriminator mapping in the variant + if ( + 'discriminator' in variantSchema && + variantSchema.discriminator && + typeof variantSchema.discriminator === 'object' && + 'mapping' in variantSchema.discriminator && + variantSchema.discriminator.mapping && + typeof variantSchema.discriminator.mapping === 'object' + ) { + const mapping = variantSchema.discriminator.mapping; + const updatedMapping: Record = {}; + + for (const [discriminatorValue, originalRef] of Object.entries(mapping)) { + const map = split.mapping[originalRef]; + if (map) { + if (context === 'read' && map.read) { + updatedMapping[discriminatorValue] = map.read; + } else if (context === 'write' && map.write) { + updatedMapping[discriminatorValue] = map.write; + } else { + updatedMapping[discriminatorValue] = originalRef; + } + } else { + updatedMapping[discriminatorValue] = originalRef; + } + } + + variantSchema.discriminator.mapping = updatedMapping; + } + + // Add the variant to split.schemas with an appropriate name + if (context === 'write') { + const writeBase = applyNaming(parentName, config.requests); + const writeName = getUniqueComponentName({ + base: writeBase, + components: existingNames, + }); + existingNames.add(writeName); + split.schemas[writeName] = variantSchema; + variants.write = `${schemasPointerNamespace}${writeName}`; + } + // We could create read variants too, but typically they're not needed + // since the original schema serves as the read variant + } + + parentSchemaVariants.set(parentRef, variants); + } + + // Third pass: update $refs in split schemas to point to the parent variants + for (const [name, schema] of Object.entries(split.schemas)) { + const pointer = `${schemasPointerNamespace}${name}`; + const originalPointer = split.reverseMapping[pointer]; + if (!originalPointer) continue; + + const mapping = split.mapping[originalPointer]; + if (!mapping) continue; + + const contextVariant: Scope | null = + mapping.read === pointer ? 'read' : mapping.write === pointer ? 'write' : null; + + if (contextVariant && schema && typeof schema === 'object') { + // Update $refs in allOf + if ('allOf' in schema && schema.allOf instanceof Array) { + for (let i = 0; i < schema.allOf.length; i++) { + const comp = schema.allOf[i]; + + if (comp && typeof comp === 'object' && '$ref' in comp && typeof comp.$ref === 'string') { + const variants = parentSchemaVariants.get(comp.$ref); + + if (variants) { + if (contextVariant === 'write' && variants.write) { + comp.$ref = variants.write; + } else if (contextVariant === 'read' && variants.read) { + comp.$ref = variants.read; + } + } + } + } + } + } + } +} + /** * Splits schemas with both 'read' and 'write' scopes into read/write variants. * Returns the new schemas and a mapping from original pointer to new variant pointers. @@ -437,183 +619,13 @@ export const splitSchemas = ({ split.reverseMapping[writePointer] = pointer; } - //Create writable variants of parent schemas that have discriminators and are referenced by split schemas - const parentSchemasToSplit = new Map>(); - - // First pass: identify parent schemas that need writable variants - for (const [name, schema] of Object.entries(split.schemas)) { - const pointer = `${schemasPointerNamespace}${name}`; - const originalPointer = split.reverseMapping[pointer]; - - if (originalPointer) { - const mapping = split.mapping[originalPointer]; - if (mapping) { - const contextVariant = - mapping.read === pointer ? 'read' : mapping.write === pointer ? 'write' : null; - - if (contextVariant && schema && typeof schema === 'object') { - // Check allOf for $refs to schemas with discriminators - if ('allOf' in schema && Array.isArray((schema as Record).allOf)) { - const allOf = (schema as Record).allOf as Array; - - for (const comp of allOf) { - if ( - comp && - typeof comp === 'object' && - '$ref' in comp && - typeof comp.$ref === 'string' - ) { - const ref = comp.$ref as string; - const schemasObj = getSchemasObject(spec); - - if (schemasObj) { - const refPath = jsonPointerToPath(ref); - const schemaName = refPath[refPath.length - 1]; - - if (typeof schemaName === 'string' && schemaName in schemasObj) { - const resolvedSchema = schemasObj[schemaName]; - - // Check if this schema has a discriminator with mapping - if ( - resolvedSchema && - typeof resolvedSchema === 'object' && - 'discriminator' in resolvedSchema && - resolvedSchema.discriminator && - typeof resolvedSchema.discriminator === 'object' && - 'mapping' in resolvedSchema.discriminator && - resolvedSchema.discriminator.mapping && - typeof resolvedSchema.discriminator.mapping === 'object' - ) { - // This parent schema needs a variant for this context - if (!parentSchemasToSplit.has(ref)) { - parentSchemasToSplit.set(ref, new Set()); - } - parentSchemasToSplit.get(ref)!.add(contextVariant); - } - } - } - } - } - } - } - } - } - } - - // Second pass: create writable variants of parent schemas and update their discriminator mappings - const parentSchemaVariants = new Map(); - - for (const [parentRef, contexts] of parentSchemasToSplit) { - const schemasObj = getSchemasObject(spec); - if (!schemasObj) continue; - - const refPath = jsonPointerToPath(parentRef); - const parentName = refPath[refPath.length - 1]; - - if (typeof parentName !== 'string' || !(parentName in schemasObj)) continue; - - const parentSchema = schemasObj[parentName]; - if (!parentSchema || typeof parentSchema !== 'object') continue; - - const variants: { read?: string; write?: string } = {}; - - // Create variants for each context - for (const context of contexts) { - const variantSchema = deepClone(parentSchema); - - // Update discriminator mapping in the variant - if ( - 'discriminator' in variantSchema && - variantSchema.discriminator && - typeof variantSchema.discriminator === 'object' && - 'mapping' in variantSchema.discriminator && - variantSchema.discriminator.mapping && - typeof variantSchema.discriminator.mapping === 'object' - ) { - const mapping = (variantSchema.discriminator as any).mapping as Record; - const updatedMapping: Record = {}; - - for (const [discriminatorValue, originalRef] of Object.entries(mapping)) { - const map = split.mapping[originalRef]; - if (map) { - if (context === 'read' && map.read) { - updatedMapping[discriminatorValue] = map.read; - } else if (context === 'write' && map.write) { - updatedMapping[discriminatorValue] = map.write; - } else { - updatedMapping[discriminatorValue] = originalRef; - } - } else { - updatedMapping[discriminatorValue] = originalRef; - } - } - - (variantSchema as any).discriminator.mapping = updatedMapping; - } - - // Add the variant to split.schemas with an appropriate name - if (context === 'write') { - const variantBase = applyNaming(parentName, config.requests); - const variantName = - variantBase === parentName - ? `${parentName}Writable` - : getUniqueComponentName({ - base: variantBase, - components: existingNames, - }); - existingNames.add(variantName); - split.schemas[variantName] = variantSchema; - variants.write = `${schemasPointerNamespace}${variantName}`; - } - // We could create read variants too, but typically they're not needed - // since the original schema serves as the read variant - } - - parentSchemaVariants.set(parentRef, variants); - } - - // Third pass: update $refs in split schemas to point to the parent variants - for (const [name, schema] of Object.entries(split.schemas)) { - const pointer = `${schemasPointerNamespace}${name}`; - const originalPointer = split.reverseMapping[pointer]; - - if (originalPointer) { - const mapping = split.mapping[originalPointer]; - if (mapping) { - const contextVariant = - mapping.read === pointer ? 'read' : mapping.write === pointer ? 'write' : null; - - if (contextVariant && schema && typeof schema === 'object') { - // Update $refs in allOf - if ('allOf' in schema && Array.isArray((schema as Record).allOf)) { - const allOf = (schema as Record).allOf as Array; - - for (let i = 0; i < allOf.length; i++) { - const comp = allOf[i]; - - if ( - comp && - typeof comp === 'object' && - '$ref' in comp && - typeof comp.$ref === 'string' - ) { - const ref = comp.$ref as string; - const variants = parentSchemaVariants.get(ref); - - if (variants) { - if (contextVariant === 'write' && variants.write) { - (comp as any).$ref = variants.write; - } else if (contextVariant === 'read' && variants.read) { - (comp as any).$ref = variants.read; - } - } - } - } - } - } - } - } - } + splitDiscriminatorSchemas({ + config, + existingNames, + schemasPointerNamespace, + spec, + split, + }); event.timeEnd(); return split; -- 2.51.2 From 7e6945d8acf3d612f9d34d68d213da8ed811d44e Mon Sep 17 00:00:00 2001 From: Lubos Date: Wed, 4 Feb 2026 07:20:42 +0800 Subject: [PATCH 6/6] chore: improve types --- .../shared/src/openApi/shared/transforms/readWrite.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/openApi/shared/transforms/readWrite.ts b/packages/shared/src/openApi/shared/transforms/readWrite.ts index 496a31bea..bfa901187 100644 --- a/packages/shared/src/openApi/shared/transforms/readWrite.ts +++ b/packages/shared/src/openApi/shared/transforms/readWrite.ts @@ -822,11 +822,9 @@ export const updateRefsInSpec = ({ } } else if (key === 'discriminator' && typeof value === 'object' && value !== null) { // Update discriminator mappings to point to the correct read/write variants - const discriminator = value as Record; - if (discriminator.mapping && typeof discriminator.mapping === 'object') { - const mapping = discriminator.mapping as Record; + if ('mapping' in value && value.mapping && typeof value.mapping === 'object') { const updatedMapping: Record = {}; - for (const [discriminatorValue, originalRef] of Object.entries(mapping)) { + for (const [discriminatorValue, originalRef] of Object.entries(value.mapping)) { const map = split.mapping[originalRef]; if (map) { if (nextContext === 'read' && map.read) { @@ -842,7 +840,7 @@ export const updateRefsInSpec = ({ updatedMapping[discriminatorValue] = originalRef; } } - discriminator.mapping = updatedMapping; + value.mapping = updatedMapping; } // Continue walking the discriminator object for other properties walk({