Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,10 @@ private OpenApiSchema CreateObjectSchema(DataContract dataContract, SchemaReposi
schema.AllOf.Add(baseTypeSchema);
}

applicableDataProperties = applicableDataProperties
// if the property is declared on a type other than (the one we just added as a base or one of its parents)
.Where(dataProperty => !baseTypeDataContract.UnderlyingType.IsAssignableTo(dataProperty.MemberInfo.DeclaringType));
// If the property is declared on a type other than the one we just added as a base (or one of its parents)
// See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/3201.
applicableDataProperties = applicableDataProperties.Where(
(dataProperty) => !baseTypeDataContract.UnderlyingType.IsAssignableTo(dataProperty.MemberInfo.DeclaringType));
}

if (IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,37 @@

namespace Swashbuckle.AspNetCore.SwaggerGen.Test;

public class FakeControllerWithInheritance
#pragma warning disable CA1822

public class FakeControllerWithInheritance : Controller
{
public void ActionWithDerivedObjectParameter([FromBody] AbcTests_C param)
{ }
public void ActionWithDerivedObjectParameter([FromBody] MostDerivedClass param)
{
System.Diagnostics.Debug.Assert(param is not null);
}

public List<AbcTests_A> ActionWithDerivedObjectResponse()
public List<BaseClass> ActionWithDerivedObjectResponse()
{
return null!;
}

public AbcTests_B ActionWithDerivedObjectResponse_ExcludedFromInheritanceConfig()
public DerivedClass ActionWithDerivedObjectResponse_ExcludedFromInheritanceConfig()
{
return null!;
}

// Helper test types for GenerateSchema_PreservesIntermediateBaseProperties_WhenUsingOneOfPolymorphism
public abstract class AbcTests_A
public abstract class BaseClass
{
public string PropA { get; set; }
public string BaseProperty { get; set; }
}

public class AbcTests_B : AbcTests_A
public class DerivedClass : BaseClass
{
public string PropB { get; set; }
public string DerivedProperty { get; set; }
}

public class AbcTests_C : AbcTests_B
public class MostDerivedClass : DerivedClass
{
public string PropC { get; set; }
public string MoreDerivedProperty { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -721,54 +721,53 @@ public void GenerateSchema_SupportsOption_UseAllOfForPolymorphism()
public void GenerateSchema_PreservesIntermediateBaseProperties_WhenUsingOneOfPolymorphism()
{
// Arrange - define a type hierarchy A <- B <- C where only A and C are selected as known subtypes
var subject = Subject(configureGenerator: c =>
var subject = Subject(configureGenerator: (c) =>
{
c.UseOneOfForPolymorphism = true;
c.SubTypesSelector = (type) => type == typeof(AbcTests_A) ? new[] { typeof(AbcTests_C) } : Array.Empty<Type>();
c.SubTypesSelector = (type) => type == typeof(ModelOfA) ? [typeof(ModelOfC)] : [];
});

var schemaRepository = new SchemaRepository();

// Act
var schema = subject.GenerateSchema(typeof(AbcTests_A), schemaRepository);
var schema = subject.GenerateSchema(typeof(ModelOfA), schemaRepository);

// Assert - polymorphic schema should be present
Assert.NotNull(schema.OneOf);

// Ensure base A schema contains PropA
Assert.True(schemaRepository.Schemas.ContainsKey(nameof(AbcTests_A)));
var aSchema = schemaRepository.Schemas[nameof(AbcTests_A)];
Assert.True(aSchema.Properties.ContainsKey(nameof(AbcTests_A.PropA)));
Assert.True(schemaRepository.Schemas.ContainsKey(nameof(ModelOfA)));
var aSchema = schemaRepository.Schemas[nameof(ModelOfA)];
Assert.True(aSchema.Properties.ContainsKey(nameof(ModelOfA.PropertyOfA)));

// Find the C schema in the OneOf and assert it preserves B's properties while not duplicating A's
var cRef = schema.OneOf
.OfType<OpenApiSchemaReference>()
.First(r => r.Reference.Id == nameof(AbcTests_C));
.First(r => r.Reference.Id == nameof(ModelOfC));

var cSchema = schemaRepository.Schemas[nameof(AbcTests_C)];
var cSchema = schemaRepository.Schemas[nameof(ModelOfC)];

// C should include PropC and properties declared on intermediate B
Assert.True(cSchema.Properties.ContainsKey(nameof(AbcTests_C.PropC)));
Assert.True(cSchema.Properties.ContainsKey(nameof(AbcTests_B.PropB)));
Assert.True(cSchema.Properties.ContainsKey(nameof(ModelOfC.PropertyOfC)));
Assert.True(cSchema.Properties.ContainsKey(nameof(ModelOfB.PropertyOfB)));

// A's property should not be in C's inline properties because it's provided by the referenced base schema
Assert.False(cSchema.Properties.ContainsKey(nameof(AbcTests_A.PropA)));
Assert.False(cSchema.Properties.ContainsKey(nameof(ModelOfA.PropertyOfA)));
}

// Helper test types for the A/B/C regression
public abstract class AbcTests_A
public abstract class ModelOfA
{
public string PropA { get; set; }
public string PropertyOfA { get; set; }
}

public class AbcTests_B : AbcTests_A
public class ModelOfB : ModelOfA
{
public string PropB { get; set; }
public string PropertyOfB { get; set; }
}

public class AbcTests_C : AbcTests_B
public class ModelOfC : ModelOfB
{
public string PropC { get; set; }
public string PropertyOfC { get; set; }
}

[Fact]
Expand Down
56 changes: 26 additions & 30 deletions test/Swashbuckle.AspNetCore.SwaggerGen.Test/VerifyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1129,26 +1129,24 @@ public async Task GenerateSchema_PreservesIntermediateBaseProperties_WhenUsingOn
{
Name = "param1",
Source = BindingSource.Body,
Type = typeof(FakeControllerWithInheritance.AbcTests_C), // most derived type
ModelMetadata = ModelMetadataFactory.CreateForType(typeof(FakeControllerWithInheritance.AbcTests_C)),
Type = typeof(FakeControllerWithInheritance.MostDerivedClass),
ModelMetadata = ModelMetadataFactory.CreateForType(typeof(FakeControllerWithInheritance.MostDerivedClass)),
},
],
supportedRequestFormats:
[
new ApiRequestFormat { MediaType = "application/json" },
]),
supportedRequestFormats: [new ApiRequestFormat { MediaType = "application/json" }]),
ApiDescriptionFactory.Create<FakeControllerWithInheritance>(
c => nameof(c.ActionWithDerivedObjectResponse),
groupName: "v1",
httpMethod: "GET",
relativePath: "resource",
parameterDescriptions: [],
supportedResponseTypes: [
supportedResponseTypes:
[
new ApiResponseType
{
ApiResponseFormats = [new ApiResponseFormat { MediaType = "application/json" }],
StatusCode = 200,
Type = typeof(FakeControllerWithInheritance.AbcTests_A),
Type = typeof(FakeControllerWithInheritance.BaseClass),
},
]),
ApiDescriptionFactory.Create<FakeControllerWithInheritance>(
Expand All @@ -1157,24 +1155,24 @@ public async Task GenerateSchema_PreservesIntermediateBaseProperties_WhenUsingOn
httpMethod: "GET",
relativePath: "resourceB",
parameterDescriptions: [],
supportedResponseTypes: [
supportedResponseTypes:
[
new ApiResponseType
{
ApiResponseFormats = [new ApiResponseFormat { MediaType = "application/json" }],
StatusCode = 200,
Type = typeof(FakeControllerWithInheritance.AbcTests_B),
Type = typeof(FakeControllerWithInheritance.DerivedClass),
},
]),
],
configureSchemaGeneratorOptions: c =>
{
c.UseOneOfForPolymorphism = true;
c.SubTypesSelector =
(type) => (Type[])(
type == typeof(FakeControllerWithInheritance.AbcTests_A)
? [typeof(FakeControllerWithInheritance.AbcTests_C)]
: []
);
(type) =>
type == typeof(FakeControllerWithInheritance.BaseClass)
? [typeof(FakeControllerWithInheritance.MostDerivedClass)]
: [];
}
);
var document = subject.GetSwagger("v1");
Expand All @@ -1199,26 +1197,24 @@ public async Task GenerateSchema_PreservesMultiLevelInheritance()
{
Name = "param1",
Source = BindingSource.Body,
Type = typeof(FakeControllerWithInheritance.AbcTests_C), // most derived type
ModelMetadata = ModelMetadataFactory.CreateForType(typeof(FakeControllerWithInheritance.AbcTests_C)),
Type = typeof(FakeControllerWithInheritance.MostDerivedClass),
ModelMetadata = ModelMetadataFactory.CreateForType(typeof(FakeControllerWithInheritance.MostDerivedClass)),
},
],
supportedRequestFormats:
[
new ApiRequestFormat { MediaType = "application/json" },
]),
supportedRequestFormats: [new ApiRequestFormat { MediaType = "application/json" }]),
ApiDescriptionFactory.Create<FakeControllerWithInheritance>(
c => nameof(c.ActionWithDerivedObjectResponse),
groupName: "v1",
httpMethod: "GET",
relativePath: "resource",
parameterDescriptions: [],
supportedResponseTypes: [
supportedResponseTypes:
[
new ApiResponseType
{
ApiResponseFormats = [new ApiResponseFormat { MediaType = "application/json" }],
StatusCode = 200,
Type = typeof(FakeControllerWithInheritance.AbcTests_A),
Type = typeof(FakeControllerWithInheritance.BaseClass),
},
]),
ApiDescriptionFactory.Create<FakeControllerWithInheritance>(
Expand All @@ -1227,24 +1223,24 @@ public async Task GenerateSchema_PreservesMultiLevelInheritance()
httpMethod: "GET",
relativePath: "resourceB",
parameterDescriptions: [],
supportedResponseTypes: [
supportedResponseTypes:
[
new ApiResponseType
{
ApiResponseFormats = [new ApiResponseFormat { MediaType = "application/json" }],
StatusCode = 200,
Type = typeof(FakeControllerWithInheritance.AbcTests_B),
Type = typeof(FakeControllerWithInheritance.DerivedClass),
},
]),
],
configureSchemaGeneratorOptions: c =>
{
c.UseOneOfForPolymorphism = true;
c.SubTypesSelector =
(type) => (Type[])(
type == typeof(FakeControllerWithInheritance.AbcTests_A) ? [typeof(FakeControllerWithInheritance.AbcTests_B), typeof(FakeControllerWithInheritance.AbcTests_C)]
: type == typeof(FakeControllerWithInheritance.AbcTests_B) ? [typeof(FakeControllerWithInheritance.AbcTests_C)]
: []
);
(type) =>
type == typeof(FakeControllerWithInheritance.BaseClass) ? [typeof(FakeControllerWithInheritance.DerivedClass), typeof(FakeControllerWithInheritance.MostDerivedClass)]
: type == typeof(FakeControllerWithInheritance.DerivedClass) ? [typeof(FakeControllerWithInheritance.MostDerivedClass)]
Comment on lines +1241 to +1242
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] This line exceeds a reasonable line length (approximately 170 characters). Consider breaking this into multiple lines for better readability, similar to the formatting used in the previous test method (lines 1172-1175).

Suggested change
type == typeof(FakeControllerWithInheritance.BaseClass) ? [typeof(FakeControllerWithInheritance.DerivedClass), typeof(FakeControllerWithInheritance.MostDerivedClass)]
: type == typeof(FakeControllerWithInheritance.DerivedClass) ? [typeof(FakeControllerWithInheritance.MostDerivedClass)]
type == typeof(FakeControllerWithInheritance.BaseClass)
? [typeof(FakeControllerWithInheritance.DerivedClass), typeof(FakeControllerWithInheritance.MostDerivedClass)]
: type == typeof(FakeControllerWithInheritance.DerivedClass)
? [typeof(FakeControllerWithInheritance.MostDerivedClass)]

Copilot uses AI. Check for mistakes.
: [];
}
);
var document = subject.GetSwagger("v1");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AbcTests_C"
"$ref": "#/components/schemas/MostDerivedClass"
}
}
}
Expand All @@ -39,7 +39,7 @@
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/AbcTests_C"
"$ref": "#/components/schemas/MostDerivedClass"
}
]
}
Expand All @@ -61,7 +61,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AbcTests_B"
"$ref": "#/components/schemas/DerivedClass"
}
}
}
Expand All @@ -72,43 +72,43 @@
},
"components": {
"schemas": {
"AbcTests_A": {
"BaseClass": {
"type": "object",
"properties": {
"PropA": {
"BaseProperty": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
},
"AbcTests_B": {
"DerivedClass": {
"type": "object",
"properties": {
"PropA": {
"BaseProperty": {
"type": "string",
"nullable": true
},
"PropB": {
"DerivedProperty": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
},
"AbcTests_C": {
"MostDerivedClass": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/AbcTests_A"
"$ref": "#/components/schemas/BaseClass"
}
],
"properties": {
"PropB": {
"DerivedProperty": {
"type": "string",
"nullable": true
},
"PropC": {
"MoreDerivedProperty": {
"type": "string",
"nullable": true
}
Expand Down
Loading
Loading