In our API we would like to return a object from a external Nuget package when a users makes a call to the endpoint.
This object (Can be viewed here) has a couple of properties. One of them is called Action. This property has as type IPaymentResponseAction but can be a set of different action types (You can see them all over here).
The generated swagger does not know about these actions and doesn't generate the required code. Even with the polymorphism setting set.
services.AddSwaggerGen(c =>
{
c.EnableAnnotations();
c.UseOneOfForPolymorphism();
});
Is there a way that i can make these objects show up in my swagger? Maybe with some custom SwaggerGenOptions?
Update after first answer with the c.SelectSubTypesUsing code
Adyen.Model.Checkout.PaymentResponse": {
"type": "object",
"properties": {
"resultCode": {
"$ref": "#/components/schemas/Adyen.Model.Checkout.PaymentResponse.ResultCodeEnum"
},
"action": {
"oneOf": [
{
"$ref": "#/components/schemas/Adyen.Model.Checkout.Action.IPaymentResponseAction"
},
{
"$ref": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutAwaitAction"
},
{
"$ref": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutDonationAction"
},
{
"$ref": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutOneTimePasscodeAction"
},
{
"$ref": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutQrCodeAction"
},
{
"$ref": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutRedirectAction"
},
{
"$ref": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutSDKAction"
},
{
"$ref": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutThreeDS2Action"
},
{
"$ref": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutVoucherAction"
}
],
"nullable": true
}......
And the IPaymentResponseAction is:
"Adyen.Model.Checkout.Action.IPaymentResponseAction": {
"required": [
"type"
],
"type": "object",
"properties": {
"type": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false,
"discriminator": {
"propertyName": "type",
"mapping": {
"await": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutAwaitAction",
"donation": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutDonationAction",
"oneTimePasscode": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutOneTimePasscodeAction",
"qrCode": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutQrCodeAction",
"redirect": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutRedirectAction",
"sdk": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutSDKAction",
"threeDS2Action": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutThreeDS2Action",
"voucher": "#/components/schemas/Adyen.Model.Checkout.Action.CheckoutVoucherAction"
}
}
},
UPDATE: All my actions look like this now, so i think its not there yet. But its close!
"CheckoutAwaitAction": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/Rig.Commercial.Reservation.Core.Settings.Swagger.Swagger_Models.PaymentResponseAction"
}
],
"additionalProperties": false
}
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).
Swashbuckle options c.EnableAnnotations(enableAnnotationsForInheritance: true, enableAnnotationsForPolymorphism: true);
c.UseAllOfToExtendReferenceSchemas();
c.UseAllOfForInheritance();
c.UseOneOfForPolymorphism();
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
};
});
allOf keyword in implementation classesUp 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))
};
});
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
This can be done using the Swashbuckle.AspNetCore.Annotations package. Depending on the API design, you can use one of the following approaches.
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.
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()
{
// ...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With