From fb8623cff51fed4bca5064f8f22f9b18b01d8118 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:27:47 +0000 Subject: [PATCH] Fix discriminator inheritance to use Omit for proper type narrowing - Remove union of child discriminator values from intermediate types - Each type now gets only its direct discriminator value - Use Omit<> to remove parent discriminator properties and avoid conflicts - Enables proper TypeScript type narrowing with discriminated unions Example: CarDto.$type is now 'Car' instead of 'Car' | 'Volvo' VolvoDto uses Omit & { $type: 'Volvo' } Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- packages/openapi-ts/src/ir/types.d.ts | 5 ++ .../src/openApi/3.0.x/parser/schema.ts | 61 +++++++++++++------ .../src/openApi/3.1.x/parser/schema.ts | 60 ++++++++++++------ .../plugins/@hey-api/typescript/v1/plugin.ts | 22 ++++++- 4 files changed, 110 insertions(+), 38 deletions(-) diff --git a/packages/openapi-ts/src/ir/types.d.ts b/packages/openapi-ts/src/ir/types.d.ts index af7f669e8..9540ae007 100644 --- a/packages/openapi-ts/src/ir/types.d.ts +++ b/packages/openapi-ts/src/ir/types.d.ts @@ -182,6 +182,11 @@ interface IRSchemaObject * @default 'or' */ logicalOperator?: 'and' | 'or'; + /** + * When used with `$ref` or `symbolRef`, specifies properties to omit from the referenced schema. + * Useful for handling discriminator property conflicts in allOf compositions. + */ + omit?: ReadonlyArray; /** * When type is `object`, `patternProperties` can be used to define a schema * for properties that match a specific regex pattern. diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts index 9813e7526..b6dfd33a1 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts @@ -80,15 +80,13 @@ const findDiscriminatorsInSchema = ({ }; /** - * Gets all discriminator values for a schema and its children in the inheritance hierarchy. - * For intermediate schemas (those that are extended by others), returns a union of all values. + * Gets the discriminator value for a schema. + * Returns only the schema's own discriminator value, not child values. */ const getAllDiscriminatorValues = ({ - context, discriminator, schemaRef, }: { - context: Context; discriminator: NonNullable; schemaRef: string; }): Array => { @@ -101,19 +99,6 @@ const getAllDiscriminatorValues = ({ if (mappedSchemaRef === schemaRef) { // This is the current schema's own value values.push(value); - continue; - } - - // Check if the mapped schema extends the current schema - const mappedSchema = context.resolveRef(mappedSchemaRef); - if (mappedSchema.allOf) { - for (const item of mappedSchema.allOf) { - if ('$ref' in item && item.$ref === schemaRef) { - // This schema extends the current schema, add its value - values.push(value); - break; - } - } } } @@ -521,7 +506,6 @@ const parseAllOf = ({ for (const { discriminator, isRequired, values } of discriminatorsToAdd) { // Get all discriminator values including children for union types const allValues = getAllDiscriminatorValues({ - context, discriminator, schemaRef: state.$ref!, }); @@ -544,6 +528,47 @@ const parseAllOf = ({ } : valueSchemas[0]!; + // Check if any $ref schemas in schemaItems have this discriminator property + // If yes, mark them to omit it to avoid conflicts + for (const item of schemaItems) { + if (item.$ref || item.symbolRef) { + // Check if the referenced schema has this property + const hasProperty = (() => { + if (!item.$ref) return false; + try { + const refSchema = context.resolveRef(item.$ref); + // Check if the discriminator property exists in the ref schema + return ( + refSchema.properties?.[discriminator.propertyName] !== + undefined || + (refSchema.allOf && + refSchema.allOf.some((allOfItem) => { + const resolved = + '$ref' in allOfItem + ? context.resolveRef(allOfItem.$ref) + : allOfItem; + return ( + resolved.properties?.[discriminator.propertyName] !== + undefined + ); + })) + ); + } catch { + return false; + } + })(); + + if (hasProperty) { + // Mark this ref to omit the discriminator property + if (!item.omit) { + item.omit = [discriminator.propertyName]; + } else if (!item.omit.includes(discriminator.propertyName)) { + item.omit = [...item.omit, discriminator.propertyName]; + } + } + } + } + // Find the inline schema (non-$ref) to merge the discriminator property into // The inline schema should be the last non-$ref item in schemaItems let inlineSchema: IR.SchemaObject | undefined; diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts index 1f20bdba4..0ba9706da 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts @@ -84,15 +84,13 @@ const findDiscriminatorsInSchema = ({ }; /** - * Gets all discriminator values for a schema and its children in the inheritance hierarchy. - * For intermediate schemas (those that are extended by others), returns a union of all values. + * Gets the discriminator value for a schema. + * Returns only the schema's own discriminator value, not child values. */ const getAllDiscriminatorValues = ({ - context, discriminator, schemaRef, }: { - context: Context; discriminator: NonNullable; schemaRef: string; }): Array => { @@ -105,19 +103,6 @@ const getAllDiscriminatorValues = ({ if (mappedSchemaRef === schemaRef) { // This is the current schema's own value values.push(value); - continue; - } - - // Check if the mapped schema extends the current schema - const mappedSchema = context.resolveRef(mappedSchemaRef); - if (mappedSchema.allOf) { - for (const item of mappedSchema.allOf) { - if (item.$ref && item.$ref === schemaRef) { - // This schema extends the current schema, add its value - values.push(value); - break; - } - } } } @@ -602,7 +587,6 @@ const parseAllOf = ({ for (const { discriminator, isRequired, values } of discriminatorsToAdd) { // Get all discriminator values including children for union types const allValues = getAllDiscriminatorValues({ - context, discriminator, schemaRef: state.$ref!, }); @@ -625,6 +609,46 @@ const parseAllOf = ({ } : valueSchemas[0]!; + // Check if any $ref schemas in schemaItems have this discriminator property + // If yes, mark them to omit it to avoid conflicts + for (const item of schemaItems) { + if (item.$ref || item.symbolRef) { + // Check if the referenced schema has this property + const hasProperty = (() => { + if (!item.$ref) return false; + try { + const refSchema = context.resolveRef(item.$ref); + // Check if the discriminator property exists in the ref schema + return ( + refSchema.properties?.[discriminator.propertyName] !== + undefined || + (refSchema.allOf && + refSchema.allOf.some((allOfItem) => { + const resolved = allOfItem.$ref + ? context.resolveRef(allOfItem.$ref) + : allOfItem; + return ( + resolved.properties?.[discriminator.propertyName] !== + undefined + ); + })) + ); + } catch { + return false; + } + })(); + + if (hasProperty) { + // Mark this ref to omit the discriminator property + if (!item.omit) { + item.omit = [discriminator.propertyName]; + } else if (!item.omit.includes(discriminator.propertyName)) { + item.omit = [...item.omit, discriminator.propertyName]; + } + } + } + } + // Find the inline schema (non-$ref) to merge the discriminator property into // The inline schema should be the last non-$ref item in schemaItems let inlineSchema: IR.SchemaObject | undefined; diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/v1/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/v1/plugin.ts index 8e8a6d642..28224e6a9 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/v1/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/v1/plugin.ts @@ -24,7 +24,16 @@ export const irSchemaToAst = ({ schema: IR.SchemaObject; }): MaybeTsDsl => { if (schema.symbolRef) { - return $.type(schema.symbolRef); + const baseType = $.type(schema.symbolRef); + if (schema.omit && schema.omit.length > 0) { + // Render as Omit + const omittedKeys = + schema.omit.length === 1 + ? $.type.literal(schema.omit[0]!) + : $.type.or(...schema.omit.map((key) => $.type.literal(key))); + return $.type('Omit').generics(baseType, omittedKeys); + } + return baseType; } if (schema.$ref) { @@ -33,7 +42,16 @@ export const irSchemaToAst = ({ resource: 'definition', resourceId: schema.$ref, }); - return $.type(symbol); + const baseType = $.type(symbol); + if (schema.omit && schema.omit.length > 0) { + // Render as Omit + const omittedKeys = + schema.omit.length === 1 + ? $.type.literal(schema.omit[0]!) + : $.type.or(...schema.omit.map((key) => $.type.literal(key))); + return $.type('Omit').generics(baseType, omittedKeys); + } + return baseType; } if (schema.type) { -- 2.51.2