PolymorphismDocumentTransformer
Namespace: RA.Utilities.OpenApi.DocumentTransformers
The PolymorphismDocumentTransformer is an IOpenApiDocumentTransformer that enables correct OpenAPI documentation for polymorphic types (a base class with one or more derived classes). It modifies the OpenAPI document to use the oneOf and discriminator keywords, which allows API clients and code generators to understand and handle the inheritance structure.
This transformer is designed for the native ASP.NET Core OpenAPI tools (Microsoft.AspNetCore.OpenApi).
🎯 Purpose
When an API endpoint can return different concrete types that share a common base class, standard OpenAPI generation often fails to capture this relationship. For example, a generic Error base class might have specific implementations like NotFoundError and ConflictError. Without special handling, the documentation would only show the base Error type, and clients would not know about the possible variations.
The PolymorphismDocumentTransformer solves this by explicitly defining the polymorphic relationship in the OpenAPI schema.
📋 Parameters
The PolymorphismDocumentTransformer is configured through its constructor.
| Parameter | Type | Required | Description |
|---|---|---|---|
| polymorphismPropertyName | string | True | The name of the base schema in the OpenAPI document that represents the polymorphic type (e.g., "ErrorResponse"). This must match the schema name generated by the OpenAPI tools. |
| typesToInclude | Dictionary<string, Type> | True | A dictionary that maps a discriminator value to a derived Type. The key is the string that will appear in the discriminator property (e.g., "NotFound"), and the value is the .NET type it maps to (e.g., typeof(NotFoundResponse)). |
| discriminatorPropertyName | string | False | The name of the property in the JSON object that will be used to differentiate between the concrete types. Defaults to "Type". |
⚙️ How It Works
- Finds the Base Schema: The transformer looks for the OpenAPI schema corresponding to the base class you specify (e.g., a schema named "Error").
- Ensures Derived Schemas Exist: It iterates through a dictionary of derived types you provide. For each one, it checks if a schema already exists in the document. If not, it generates one.
- Adds
oneOfReference: It modifies the base schema by adding aoneOfproperty. This property contains a list of references to the schemas of all the derived types. This tells clients that the actual object will be one of the types in this list. - Adds a
discriminator: It adds adiscriminatorobject to the base schema. The discriminator specifies a property name (e.g.,"type") that clients can use to determine which derived type is being used. It also provides a mapping from the value of that property to the correct schema.
The final result in the OpenAPI JSON looks something like this:
"Error": {
"type": "object",
"oneOf": [
{ "$ref": "#/components/schemas/NotFoundError" },
{ "$ref": "#/components/schemas/ConflictError" }
],
"discriminator": {
"propertyName": "type",
"mapping": {
"NotFound": "#/components/schemas/NotFoundError",
"Conflict": "#/components/schemas/ConflictError"
}
}
}
🚀 Usage
Because the PolymorphismDocumentTransformer needs to be configured with the specific base and derived types you're using, it must be instantiated manually and registered in Program.cs.
Register the Transformer in Program.cs
In this example, let's assume you have a base Error class and two derived classes, NotFoundResponse and ConflictResponse.
// Program.cs
using RA.Utilities.Api.Results;
using RA.Utilities.OpenApi.DocumentTransformers;
using RA.Utilities.OpenApi.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi();
// Instantiate and register the transformer
builder.Services.AddOpenApiDocumentTransformer<ErrorResponse>(
typesToInclude: new()
{
// Maps the discriminator value to the derived type
{ "NotFound", typeof(NotFoundResponse) },
{ "Conflict", typeof(ConflictResponse) },
},
discriminatorPropertyName: "responseType" // The property used to differentiate types
);
var app = builder.Build();
// ...
With this transformer enabled, your Swagger UI and generated clients will correctly understand that an endpoint returning an ErrorResponse can actually be a NotFoundResponse or a ConflictResponse, and they will know how to deserialize it correctly based on the responseType property.