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 8dd529c55..c252d6cf8 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 @@ -79,6 +79,42 @@ const findDiscriminatorsInSchema = ({ return discriminators; }; +/** + * Checks if a schema is extended by other schemas in the given discriminator mapping. + * This is used to determine if a schema is a leaf node in the inheritance hierarchy. + */ +const isSchemaExtendedInMapping = ({ + context, + discriminator, + schemaRef, +}: { + context: Context; + discriminator: NonNullable; + schemaRef: string; +}): boolean => { + // Check each schema in the discriminator mapping + for (const mappedSchemaRef of Object.values(discriminator.mapping || {})) { + // Skip if it's the same schema + if (mappedSchemaRef === schemaRef) { + continue; + } + + // Resolve the mapped schema + const mappedSchema = context.resolveRef(mappedSchemaRef); + + // Check if the mapped schema extends our schema via allOf + if (mappedSchema.allOf) { + for (const item of mappedSchema.allOf) { + if ('$ref' in item && item.$ref === schemaRef) { + return true; + } + } + } + } + + return false; +}; + const parseSchemaJsDoc = ({ irSchema, schema, @@ -419,49 +455,59 @@ const parseAllOf = ({ ); if (values.length > 0) { - const valueSchemas: ReadonlyArray = values.map( - (value) => ({ - const: value, - type: 'string', - }), - ); - const irDiscriminatorSchema: IR.SchemaObject = { - properties: { - [discriminator.propertyName]: - valueSchemas.length > 1 - ? { - items: valueSchemas, - logicalOperator: 'or', - } - : valueSchemas[0]!, - }, - type: 'object', - }; - - // Check if the discriminator property is required in any of the discriminator schemas - // by looking at all discriminators with the same property name - const isRequired = discriminators.some( - (d) => - d.discriminator.propertyName === discriminator.propertyName && - // Check in the ref's required array or in the allOf components - (ref.required?.includes(d.discriminator.propertyName) || - (ref.allOf && - ref.allOf.some((item) => { - const resolvedItem = - '$ref' in item - ? context.resolveRef(item.$ref) - : item; - return resolvedItem.required?.includes( - d.discriminator.propertyName, - ); - }))), - ); - - if (isRequired) { - irDiscriminatorSchema.required = [discriminator.propertyName]; + // Check if the current schema is extended by other schemas in this discriminator mapping + // If so, don't add the discriminator value to avoid conflicts in multi-level inheritance + const isExtended = isSchemaExtendedInMapping({ + context, + discriminator, + schemaRef: state.$ref, + }); + + if (!isExtended) { + const valueSchemas: ReadonlyArray = values.map( + (value) => ({ + const: value, + type: 'string', + }), + ); + const irDiscriminatorSchema: IR.SchemaObject = { + properties: { + [discriminator.propertyName]: + valueSchemas.length > 1 + ? { + items: valueSchemas, + logicalOperator: 'or', + } + : valueSchemas[0]!, + }, + type: 'object', + }; + + // Check if the discriminator property is required in any of the discriminator schemas + // by looking at all discriminators with the same property name + const isRequired = discriminators.some( + (d) => + d.discriminator.propertyName === discriminator.propertyName && + // Check in the ref's required array or in the allOf components + (ref.required?.includes(d.discriminator.propertyName) || + (ref.allOf && + ref.allOf.some((item) => { + const resolvedItem = + '$ref' in item + ? context.resolveRef(item.$ref) + : item; + return resolvedItem.required?.includes( + d.discriminator.propertyName, + ); + }))), + ); + + if (isRequired) { + irDiscriminatorSchema.required = [discriminator.propertyName]; + } + schemaItems.push(irDiscriminatorSchema); + addedDiscriminators.add(discriminator.propertyName); } - schemaItems.push(irDiscriminatorSchema); - addedDiscriminators.add(discriminator.propertyName); } } } 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 1cc1dd616..4bf0fc603 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 @@ -83,6 +83,42 @@ const findDiscriminatorsInSchema = ({ return discriminators; }; +/** + * Checks if a schema is extended by other schemas in the given discriminator mapping. + * This is used to determine if a schema is a leaf node in the inheritance hierarchy. + */ +const isSchemaExtendedInMapping = ({ + context, + discriminator, + schemaRef, +}: { + context: Context; + discriminator: NonNullable; + schemaRef: string; +}): boolean => { + // Check each schema in the discriminator mapping + for (const mappedSchemaRef of Object.values(discriminator.mapping || {})) { + // Skip if it's the same schema + if (mappedSchemaRef === schemaRef) { + continue; + } + + // Resolve the mapped schema + const mappedSchema = context.resolveRef(mappedSchemaRef); + + // Check if the mapped schema extends our schema via allOf + if (mappedSchema.allOf) { + for (const item of mappedSchema.allOf) { + if (item.$ref && item.$ref === schemaRef) { + return true; + } + } + } + } + + return false; +}; + const parseSchemaJsDoc = ({ irSchema, schema, @@ -501,48 +537,58 @@ const parseAllOf = ({ ); if (values.length > 0) { - const valueSchemas: ReadonlyArray = values.map( - (value) => ({ - const: value, - type: 'string', - }), - ); - const irDiscriminatorSchema: IR.SchemaObject = { - properties: { - [discriminator.propertyName]: - valueSchemas.length > 1 - ? { - items: valueSchemas, - logicalOperator: 'or', - } - : valueSchemas[0]!, - }, - type: 'object', - }; - - // Check if the discriminator property is required in any of the discriminator schemas - // by looking at all discriminators with the same property name - const isRequired = discriminators.some( - (d) => - d.discriminator.propertyName === discriminator.propertyName && - // Check in the ref's required array or in the allOf components - (ref.required?.includes(d.discriminator.propertyName) || - (ref.allOf && - ref.allOf.some((item) => { - const resolvedItem = item.$ref - ? context.resolveRef(item.$ref) - : item; - return resolvedItem.required?.includes( - d.discriminator.propertyName, - ); - }))), - ); - - if (isRequired) { - irDiscriminatorSchema.required = [discriminator.propertyName]; + // Check if the current schema is extended by other schemas in this discriminator mapping + // If so, don't add the discriminator value to avoid conflicts in multi-level inheritance + const isExtended = isSchemaExtendedInMapping({ + context, + discriminator, + schemaRef: state.$ref, + }); + + if (!isExtended) { + const valueSchemas: ReadonlyArray = values.map( + (value) => ({ + const: value, + type: 'string', + }), + ); + const irDiscriminatorSchema: IR.SchemaObject = { + properties: { + [discriminator.propertyName]: + valueSchemas.length > 1 + ? { + items: valueSchemas, + logicalOperator: 'or', + } + : valueSchemas[0]!, + }, + type: 'object', + }; + + // Check if the discriminator property is required in any of the discriminator schemas + // by looking at all discriminators with the same property name + const isRequired = discriminators.some( + (d) => + d.discriminator.propertyName === discriminator.propertyName && + // Check in the ref's required array or in the allOf components + (ref.required?.includes(d.discriminator.propertyName) || + (ref.allOf && + ref.allOf.some((item) => { + const resolvedItem = item.$ref + ? context.resolveRef(item.$ref) + : item; + return resolvedItem.required?.includes( + d.discriminator.propertyName, + ); + }))), + ); + + if (isRequired) { + irDiscriminatorSchema.required = [discriminator.propertyName]; + } + schemaItems.push(irDiscriminatorSchema); + addedDiscriminators.add(discriminator.propertyName); } - schemaItems.push(irDiscriminatorSchema); - addedDiscriminators.add(discriminator.propertyName); } } } diff --git a/specs/3.0.x/discriminator-allof-nested.json b/specs/3.0.x/discriminator-allof-nested.json index b86272706..a884a2474 100644 --- a/specs/3.0.x/discriminator-allof-nested.json +++ b/specs/3.0.x/discriminator-allof-nested.json @@ -56,21 +56,11 @@ { "$ref": "#/components/schemas/VehicleDto" }, { "type": "object", - "required": ["$type", "modelName"], + "required": ["modelName"], "properties": { - "$type": { - "type": "string" - }, "modelName": { "type": "string" } - }, - "discriminator": { - "propertyName": "$type", - "mapping": { - "Car": "#/components/schemas/CarDto", - "Volvo": "#/components/schemas/VolvoDto" - } } } ] @@ -80,7 +70,7 @@ { "$ref": "#/components/schemas/CarDto" }, { "type": "object", - "required": ["$type", "seatbeltCount"], + "required": ["seatbeltCount"], "properties": { "seatbeltCount": { "type": "integer" diff --git a/specs/3.1.x/discriminator-allof-nested.json b/specs/3.1.x/discriminator-allof-nested.json index f4d43315b..881d34b93 100644 --- a/specs/3.1.x/discriminator-allof-nested.json +++ b/specs/3.1.x/discriminator-allof-nested.json @@ -56,21 +56,11 @@ { "$ref": "#/components/schemas/VehicleDto" }, { "type": "object", - "required": ["$type", "modelName"], + "required": ["modelName"], "properties": { - "$type": { - "type": "string" - }, "modelName": { "type": "string" } - }, - "discriminator": { - "propertyName": "$type", - "mapping": { - "Car": "#/components/schemas/CarDto", - "Volvo": "#/components/schemas/VolvoDto" - } } } ] @@ -80,7 +70,7 @@ { "$ref": "#/components/schemas/CarDto" }, { "type": "object", - "required": ["$type", "seatbeltCount"], + "required": ["seatbeltCount"], "properties": { "seatbeltCount": { "type": "integer"