Skip to main content

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.

ParameterTypeRequiredDescription
polymorphismPropertyNamestringTrueThe 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.
typesToIncludeDictionary<string, Type>TrueA 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)).
discriminatorPropertyNamestringFalseThe name of the property in the JSON object that will be used to differentiate between the concrete types. Defaults to "Type".

⚙️ How It Works

  1. Finds the Base Schema: The transformer looks for the OpenAPI schema corresponding to the base class you specify (e.g., a schema named "Error").
  2. 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.
  3. Adds oneOf Reference: It modifies the base schema by adding a oneOf property. 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.
  4. Adds a discriminator: It adds a discriminator object 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.