Updated answer
This is the updated answer to the question that addresses the question :) And sorry for the long post.
The issue you described is caused by a lack of Swashbuckle
's ability to handle C# interfaces to reflect polymorphic hierarchy (seems like a missing feature to me).
Here is a workaround (see an MVP project here).
Step 1. Swashbuckle
options
c.EnableAnnotations(enableAnnotationsForInheritance: true, enableAnnotationsForPolymorphism: true);
c.UseAllOfToExtendReferenceSchemas();
c.UseAllOfForInheritance();
c.UseOneOfForPolymorphism();
Step 2. Discriminator options
Swashbuckle
does not consider interfaces as "parent" types. What if we make it "think" it's still dealing with a class and not an interface? Let's introduce PaymentResponseAction
class:
[DataContract]
[SwaggerDiscriminator("type")]
public class PaymentResponseAction : IPaymentResponseAction
{
[JsonProperty(PropertyName = "type")]
public string Type { get; set; }
}
In the AddSwaggerGen
call, we should also provide correct discriminator options:
c.SelectDiscriminatorNameUsing(type =>
{
return type.Name switch
{
nameof(PaymentResponseAction) => "type",
_ => null
};
});
c.SelectDiscriminatorValueUsing(subType =>
{
return subType.Name switch
{
nameof(CheckoutAwaitAction) => "await",
nameof(CheckoutBankTransferAction) => "bank",
nameof(CheckoutDonationAction) => "donation",
nameof(CheckoutOneTimePasscodeAction) => "oneTimePasscode",
// rest of the action types ...
_ => null
};
});
Step 3. allOf
keyword in implementation classes
Up to this point, everything almost works. The only thing that is missing is the allOf
keyword for the implementation classes. Currently, it's impossible to make it work with only Swashbuckle
's options because it uses BaseType to resolve sub-types while constructing allOf
.
And as before, we can make Swashbuckle
think that it deals with inherited types. We can generate "fake" types that inherit our new PaymentResponseAction
class and copy over properties from the implementation types we are interested in. These "fake" types don't have to be functional; they should contain enough type information to make Swashbuckle
happy.
Here is an example of a method that does it. It accepts a source type to copy properties from a base type and returns a new type. It also copies custom attributes to play well with dependent settings like AddSwaggerGenNewtonsoftSupport
.
Please note that this code should be improved to be production-ready; for example, it shouldn't "copy" public properties with JsonIgnore
or similar attributes.
private static Type GenerateReparentedType(Type originalType, Type parent)
{
var assemblyBuilder =
AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("hack"), AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("hack");
var typeBuilder = moduleBuilder.DefineType(originalType.Name, TypeAttributes.Public, parent);
foreach (var property in originalType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
var newProperty = typeBuilder
.DefineProperty(property.Name, property.Attributes, property.PropertyType, null);
var getMethod = property.GetMethod;
if (getMethod is not null)
{
var getMethodBuilder = typeBuilder
.DefineMethod(getMethod.Name, getMethod.Attributes, getMethod.ReturnType, Type.EmptyTypes);
getMethodBuilder.GetILGenerator().Emit(OpCodes.Ret);
newProperty.SetGetMethod(getMethodBuilder);
}
var setMethod = property.SetMethod;
if (setMethod is not null)
{
var setMethodBuilder = typeBuilder
.DefineMethod(setMethod.Name, setMethod.Attributes, setMethod.ReturnType, Type.EmptyTypes);
setMethodBuilder.GetILGenerator().Emit(OpCodes.Ret);
newProperty.SetSetMethod(setMethodBuilder);
}
var customAttributes = CustomAttributeData.GetCustomAttributes(property).ToArray();
foreach (var customAttributeData in customAttributes)
{
newProperty.SetCustomAttribute(DefineCustomAttribute(customAttributeData));
}
}
var type = typeBuilder.CreateType();
return type ?? throw new InvalidOperationException($"Unable to generate a re-parented type for {originalType}.");
}
private static CustomAttributeBuilder DefineCustomAttribute(CustomAttributeData attributeData)
{
// based on https://stackoverflow.com/a/3916313/8607180
var constructorArguments = attributeData.ConstructorArguments
.Select(argument => argument.Value)
.ToArray();
var propertyArguments = new List<PropertyInfo>();
var propertyArgumentValues = new List<object?>();
var fieldArguments = new List<FieldInfo>();
var fieldArgumentValues = new List<object?>();
foreach (var argument in attributeData.NamedArguments ?? Array.Empty<CustomAttributeNamedArgument>())
{
var fieldInfo = argument.MemberInfo as FieldInfo;
var propertyInfo = argument.MemberInfo as PropertyInfo;
if (fieldInfo != null)
{
fieldArguments.Add(fieldInfo);
fieldArgumentValues.Add(argument.TypedValue.Value);
}
else if (propertyInfo != null)
{
propertyArguments.Add(propertyInfo);
propertyArgumentValues.Add(argument.TypedValue.Value);
}
}
return new CustomAttributeBuilder(
attributeData.Constructor, constructorArguments,
propertyArguments.ToArray(), propertyArgumentValues.ToArray(),
fieldArguments.ToArray(), fieldArgumentValues.ToArray()
);
}
Now we can use it in the AddSwaggerGen
call to make Swashbuckle
resolve those types the way we want:
var actionTypes = new[]
{
GenerateReparentedType(typeof(CheckoutAwaitAction), typeof(PaymentResponseAction)),
GenerateReparentedType(typeof(CheckoutBankTransferAction), typeof(PaymentResponseAction)),
GenerateReparentedType(typeof(CheckoutDonationAction), typeof(PaymentResponseAction)),
GenerateReparentedType(typeof(CheckoutOneTimePasscodeAction), typeof(PaymentResponseAction)),
// rest of the action types ...
};
c.SelectSubTypesUsing(type =>
{
var allTypes = typeof(Startup).Assembly.GetTypes().ToArray();
return type.Name switch
{
nameof(PaymentResponseAction) => new[] { typeof(PaymentResponseAction) }.Union(actionTypes),
nameof(IPaymentResponseAction) => new[] { typeof(PaymentResponseAction) }.Union(actionTypes),
_ => allTypes.Where(t => t.IsSubclassOf(type))
};
});
Results
Now Swashbuckle
should generate everything correctly:
paths:
/api/someEndpoint:
get:
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PaymentResponse'
# ...
components:
schemas:
PaymentResponse:
type: object
properties:
resultCode:
allOf:
- $ref: '#/components/schemas/ResultCodeEnum'
nullable: true
action:
oneOf:
- $ref: '#/components/schemas/PaymentResponseAction'
- $ref: '#/components/schemas/CheckoutAwaitAction'
- $ref: '#/components/schemas/CheckoutBankTransferAction'
- $ref: '#/components/schemas/CheckoutDonationAction'
- $ref: '#/components/schemas/CheckoutOneTimePasscodeAction'
# ... rest of the actions
nullable: true
# ... rest of the properties
PaymentResponseAction:
required:
- type
type: object
properties:
type:
type: string
nullable: true
additionalProperties: false
discriminator:
propertyName: type
mapping:
await: '#/components/schemas/CheckoutAwaitAction'
bank: '#/components/schemas/CheckoutBankTransferAction'
donation: '#/components/schemas/CheckoutDonationAction'
oneTimePasscode: '#/components/schemas/CheckoutOneTimePasscodeAction'
# ... rest of the action mapping
CheckoutAwaitAction:
type: object
allOf:
- $ref: '#/components/schemas/PaymentResponseAction'
properties:
# CheckoutAwaitAction's own properties
additionalProperties: false
CheckoutBankTransferAction:
type: object
allOf:
- $ref: '#/components/schemas/PaymentResponseAction'
properties:
# CheckoutBankTransferAction's own properties
additionalProperties: false
CheckoutDonationAction:
type: object
allOf:
- $ref: '#/components/schemas/PaymentResponseAction'
properties:
# CheckoutDonationAction's own properties
additionalProperties: false
CheckoutOneTimePasscodeAction:
type: object
allOf:
- $ref: '#/components/schemas/PaymentResponseAction'
properties:
# CheckoutOneTimePasscodeAction's own properties
additionalProperties: false
# ... rest of the action classes
Previous (incomplete) answer
This can be done using the Swashbuckle.AspNetCore.Annotations
package. Depending on the API design, you can use one of the following approaches.
Response schema doesn't depend on a response code
This approach takes advantage of using oneOf in the response schema. The idea is to make Swashbuckle generate a response schema that would have oneOf
:
responses:
'200':
description: Success
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/CheckoutAwaitAction'
- $ref: '#/components/schemas/CheckoutBankTransferAction'
- $ref: '#/components/schemas/CheckoutDonationAction'
- $ref: '#/components/schemas/CheckoutOneTimePasscodeAction'
# ...
Here is what you need to do:
Add UseOneOfForPolymorphism
and SelectSubTypesUsing
options to your AddSwaggerGen
call; make sure your SelectSubTypesUsing
resolves IPaymentResponseAction
interface to all the desired implementations your API is returning from a controller method:
services.AddSwaggerGen(c =>
{
// ...
c.UseOneOfForPolymorphism();
c.SelectSubTypesUsing(baseType =>
{
if (baseType == typeof(IPaymentResponseAction))
{
return new[]
{
typeof(CheckoutAwaitAction),
typeof(CheckoutBankTransferAction),
typeof(CheckoutDonationAction),
typeof(CheckoutOneTimePasscodeAction),
// ...
};
}
return Enumerable.Empty<Type>();
});
Add SwaggerResponse
annotation to your controller methods. Specify only the IPaymentResponseAction
interface.
[HttpGet]
[SwaggerResponse((int)HttpStatusCode.OK, "response description", typeof(IPaymentResponseAction))]
public IPaymentResponseAction GetPaymentAction()
{
// ...
This will give you the desired schema in Swagger-UI
:
Response in Swagger-UI
Please note that Swagger-UI
doesn't support the "Example Value" section if the schema has a oneOf
definition: it will just show a response sample for the first resolved type in the SelectSubTypesUsing
call.
Response schema depends on a response code
It doesn't seem like your case, but I still wanted to mention it as an option.
If the response schema is different for different response codes, you can specify corresponding types directly in the controller:
[HttpPost]
[SwaggerResponse((int)HttpStatusCode.Created, "response description", typeof(CheckoutAwaitAction))]
[SwaggerResponse((int)HttpStatusCode.OK, "response description", typeof(CheckoutBankTransferAction))]
// ...
public IPaymentResponseAction PostPaymentAction()
{
// ...