diff --git a/src/Refitter.Core/CSharpClientGeneratorFactory.cs b/src/Refitter.Core/CSharpClientGeneratorFactory.cs
index 279b6958..0955660c 100644
--- a/src/Refitter.Core/CSharpClientGeneratorFactory.cs
+++ b/src/Refitter.Core/CSharpClientGeneratorFactory.cs
@@ -19,6 +19,7 @@ public CustomCSharpClientGenerator Create()
}
}
+ ConvertOneOfWithDiscriminatorToAllOf();
FixMissingTypesWithIntegerFormat();
ApplyCustomIntegerType();
@@ -65,6 +66,48 @@ public CustomCSharpClientGenerator Create()
return generator;
}
+ ///
+ /// Converts schemas that use oneOf/anyOf with a discriminator to use the allOf inheritance
+ /// pattern that NSwag's C# code generator understands. Without this transformation,
+ /// NSwag generates undefined anonymous types (e.g., "IdentityProvider2") instead of
+ /// proper base class references.
+ ///
+ private void ConvertOneOfWithDiscriminatorToAllOf()
+ {
+ foreach (var kvp in document.Components.Schemas)
+ {
+ var schema = kvp.Value.ActualSchema;
+
+ if (schema.DiscriminatorObject == null)
+ continue;
+
+ var unionSchemas = schema.OneOf.Concat(schema.AnyOf).ToArray();
+ if (unionSchemas.Length == 0)
+ continue;
+
+ // Ensure the base schema is typed as an object
+ if (schema.Type == JsonObjectType.None || schema.Type == JsonObjectType.Null)
+ schema.Type = JsonObjectType.Object;
+
+ // For each subtype, add allOf pointing to the base schema if not already present
+ foreach (var subSchemaRef in unionSchemas)
+ {
+ var subSchema = subSchemaRef.ActualSchema;
+ bool alreadyInherits = subSchema.AllOf.Any(
+ a => a.HasReference && a.ActualSchema == schema);
+ if (!alreadyInherits)
+ {
+ var reference = new JsonSchema { Reference = schema };
+ subSchema.AllOf.Add(reference);
+ }
+ }
+
+ // Remove the oneOf/anyOf from the base schema now that subtypes use allOf
+ schema.OneOf.Clear();
+ schema.AnyOf.Clear();
+ }
+ }
+
private void FixMissingTypesWithIntegerFormat()
{
ProcessSchemasForMissingTypes(document.Components.Schemas);
diff --git a/src/Refitter.Tests/Examples/OneOfDiscriminatorTests.cs b/src/Refitter.Tests/Examples/OneOfDiscriminatorTests.cs
new file mode 100644
index 00000000..653bef3d
--- /dev/null
+++ b/src/Refitter.Tests/Examples/OneOfDiscriminatorTests.cs
@@ -0,0 +1,283 @@
+using FluentAssertions;
+using Refitter.Core;
+using Refitter.Tests.Build;
+using Refitter.Tests.TestUtilities;
+using TUnit.Core;
+
+namespace Refitter.Tests.Examples;
+
+public class OneOfDiscriminatorTests
+{
+ private const string OpenApiSpec = @"
+openapi: 3.0.1
+info:
+ title: Test
+ version: v1
+paths:
+ /api/identityproviders:
+ get:
+ operationId: GetIdentityProvider
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IdentityProviderResponse'
+components:
+ schemas:
+ IdentityProviderResponse:
+ type: object
+ properties:
+ identityProvider:
+ $ref: '#/components/schemas/IdentityProvider'
+ identityProviders:
+ type: array
+ items:
+ $ref: '#/components/schemas/IdentityProvider'
+ IdentityProvider:
+ oneOf:
+ - $ref: '#/components/schemas/AppleIdentityProvider'
+ - $ref: '#/components/schemas/FacebookIdentityProvider'
+ discriminator:
+ propertyName: type
+ mapping:
+ Apple: '#/components/schemas/AppleIdentityProvider'
+ Facebook: '#/components/schemas/FacebookIdentityProvider'
+ AppleIdentityProvider:
+ type: object
+ properties:
+ type:
+ type: string
+ bundleId:
+ type: string
+ FacebookIdentityProvider:
+ type: object
+ properties:
+ type:
+ type: string
+ appId:
+ type: string
+";
+
+ private const string AnyOfOpenApiSpec = @"
+openapi: 3.0.1
+info:
+ title: Test
+ version: v1
+paths:
+ /api/notifications:
+ get:
+ operationId: GetNotification
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/NotificationResponse'
+components:
+ schemas:
+ NotificationResponse:
+ type: object
+ properties:
+ notification:
+ $ref: '#/components/schemas/Notification'
+ notifications:
+ type: array
+ items:
+ $ref: '#/components/schemas/Notification'
+ Notification:
+ anyOf:
+ - $ref: '#/components/schemas/EmailNotification'
+ - $ref: '#/components/schemas/SmsNotification'
+ discriminator:
+ propertyName: channel
+ mapping:
+ Email: '#/components/schemas/EmailNotification'
+ Sms: '#/components/schemas/SmsNotification'
+ EmailNotification:
+ type: object
+ properties:
+ channel:
+ type: string
+ emailAddress:
+ type: string
+ SmsNotification:
+ type: object
+ properties:
+ channel:
+ type: string
+ phoneNumber:
+ type: string
+";
+
+ [Test]
+ public async Task Can_Generate_Code()
+ {
+ string generatedCode = await GenerateCode();
+ generatedCode.Should().NotBeNullOrWhiteSpace();
+ }
+
+ [Test]
+ public async Task Generates_Base_Type_For_OneOf_With_Discriminator()
+ {
+ string generatedCode = await GenerateCode();
+ generatedCode.Should().Contain("class IdentityProvider");
+ }
+
+ [Test]
+ public async Task Does_Not_Generate_Anonymous_Type_For_OneOf_With_Discriminator()
+ {
+ string generatedCode = await GenerateCode();
+ generatedCode.Should().NotContain("IdentityProvider2");
+ }
+
+ [Test]
+ public async Task Generates_All_Subtypes_For_OneOf_With_Discriminator()
+ {
+ string generatedCode = await GenerateCode();
+ generatedCode.Should().Contain("class AppleIdentityProvider");
+ generatedCode.Should().Contain("class FacebookIdentityProvider");
+ }
+
+ [Test]
+ public async Task Generates_Inheritance_Hierarchy_For_OneOf_With_Discriminator()
+ {
+ string generatedCode = await GenerateCode();
+ generatedCode.Should().Contain("AppleIdentityProvider : IdentityProvider");
+ generatedCode.Should().Contain("FacebookIdentityProvider : IdentityProvider");
+ }
+
+ [Test]
+ public async Task Generates_Correct_Property_Type_For_OneOf_With_Discriminator()
+ {
+ string generatedCode = await GenerateCode();
+ generatedCode.Should().Contain("IdentityProvider IdentityProvider");
+ generatedCode.Should().Contain("ICollection IdentityProviders");
+ }
+
+ [Test]
+ public async Task Can_Build_Generated_Code()
+ {
+ string generatedCode = await GenerateCode();
+ BuildHelper
+ .BuildCSharp(generatedCode)
+ .Should()
+ .BeTrue();
+ }
+
+ [Test]
+ public async Task Can_Generate_Code_With_Polymorphic_Serialization()
+ {
+ string generatedCode = await GenerateCode(usePolymorphicSerialization: true);
+ generatedCode.Should().NotBeNullOrWhiteSpace();
+ }
+
+ [Test]
+ public async Task Generates_JsonPolymorphic_Attributes_For_OneOf_With_Discriminator()
+ {
+ string generatedCode = await GenerateCode(usePolymorphicSerialization: true);
+ generatedCode.Should().Contain("[JsonPolymorphic(TypeDiscriminatorPropertyName = \"type\"");
+ generatedCode.Should().Contain("[JsonDerivedType(typeof(AppleIdentityProvider)");
+ generatedCode.Should().Contain("[JsonDerivedType(typeof(FacebookIdentityProvider)");
+ }
+
+ [Test]
+ public async Task Can_Build_Generated_Code_With_Polymorphic_Serialization()
+ {
+ string generatedCode = await GenerateCode(usePolymorphicSerialization: true);
+ BuildHelper
+ .BuildCSharp(generatedCode)
+ .Should()
+ .BeTrue();
+ }
+
+ [Test]
+ public async Task Can_Generate_Code_For_AnyOf_With_Discriminator()
+ {
+ string generatedCode = await GenerateCode(spec: AnyOfOpenApiSpec);
+ generatedCode.Should().NotBeNullOrWhiteSpace();
+ }
+
+ [Test]
+ public async Task Generates_Base_Type_For_AnyOf_With_Discriminator()
+ {
+ string generatedCode = await GenerateCode(spec: AnyOfOpenApiSpec);
+ generatedCode.Should().Contain("class Notification");
+ }
+
+ [Test]
+ public async Task Does_Not_Generate_Anonymous_Type_For_AnyOf_With_Discriminator()
+ {
+ string generatedCode = await GenerateCode(spec: AnyOfOpenApiSpec);
+ generatedCode.Should().NotContain("Notification2");
+ }
+
+ [Test]
+ public async Task Generates_All_Subtypes_For_AnyOf_With_Discriminator()
+ {
+ string generatedCode = await GenerateCode(spec: AnyOfOpenApiSpec);
+ generatedCode.Should().Contain("class EmailNotification");
+ generatedCode.Should().Contain("class SmsNotification");
+ }
+
+ [Test]
+ public async Task Generates_Inheritance_Hierarchy_For_AnyOf_With_Discriminator()
+ {
+ string generatedCode = await GenerateCode(spec: AnyOfOpenApiSpec);
+ generatedCode.Should().Contain("EmailNotification : Notification");
+ generatedCode.Should().Contain("SmsNotification : Notification");
+ }
+
+ [Test]
+ public async Task Generates_Correct_Property_Type_For_AnyOf_With_Discriminator()
+ {
+ string generatedCode = await GenerateCode(spec: AnyOfOpenApiSpec);
+ generatedCode.Should().Contain("Notification Notification");
+ generatedCode.Should().Contain("ICollection Notifications");
+ }
+
+ [Test]
+ public async Task Can_Build_Generated_Code_For_AnyOf_With_Discriminator()
+ {
+ string generatedCode = await GenerateCode(spec: AnyOfOpenApiSpec);
+ BuildHelper
+ .BuildCSharp(generatedCode)
+ .Should()
+ .BeTrue();
+ }
+
+ [Test]
+ public async Task Generates_JsonPolymorphic_Attributes_For_AnyOf_With_Discriminator()
+ {
+ string generatedCode = await GenerateCode(spec: AnyOfOpenApiSpec, usePolymorphicSerialization: true);
+ generatedCode.Should().Contain("[JsonPolymorphic(TypeDiscriminatorPropertyName = \"channel\"");
+ generatedCode.Should().Contain("[JsonDerivedType(typeof(EmailNotification)");
+ generatedCode.Should().Contain("[JsonDerivedType(typeof(SmsNotification)");
+ }
+
+ [Test]
+ public async Task Can_Build_Generated_Code_For_AnyOf_With_Discriminator_And_Polymorphic_Serialization()
+ {
+ string generatedCode = await GenerateCode(spec: AnyOfOpenApiSpec, usePolymorphicSerialization: true);
+ BuildHelper
+ .BuildCSharp(generatedCode)
+ .Should()
+ .BeTrue();
+ }
+
+ private static async Task GenerateCode(string spec = OpenApiSpec, bool usePolymorphicSerialization = false)
+ {
+ var swaggerFile = await SwaggerFileHelper.CreateSwaggerFile(spec);
+ var settings = new RefitGeneratorSettings
+ {
+ OpenApiPath = swaggerFile,
+ UsePolymorphicSerialization = usePolymorphicSerialization,
+ };
+
+ var sut = await RefitGenerator.CreateAsync(settings);
+ var generatedCode = sut.Generate();
+ return generatedCode;
+ }
+}