How to Include Subclasses in Swagger API Documentation/ Openapi Specification Using Swashbuckle

How do I include subclasses in Swagger API documentation/ OpenAPI specification using Swashbuckle?

It seems Swashbuckle doesn't implement polymorphism correctly and I understand the point of view of the author about subclasses as parameters (if an action expects an Animal class and behaves differently if you call it with a dog object or a cat object, then you should have 2 different actions...) but as return types I believe that it is correct to return Animal and the objects could be Dog or Cat types.

So to describe my API and produce a proper JSON schema in line with correct guidelines (be aware of the way I describe the disciminator, if you have your own discriminator you may need to change that part in particular), I use document and schema filters as follows:

SwaggerDocsConfig configuration;
.....
configuration.DocumentFilter<PolymorphismDocumentFilter<YourBaseClass>>();
configuration.SchemaFilter<PolymorphismSchemaFilter<YourBaseClass>>();
.....

public class PolymorphismSchemaFilter<T> : ISchemaFilter
{
private readonly Lazy<HashSet<Type>> derivedTypes = new Lazy<HashSet<Type>>(Init);

private static HashSet<Type> Init()
{
var abstractType = typeof(T);
var dTypes = abstractType.Assembly
.GetTypes()
.Where(x => abstractType != x && abstractType.IsAssignableFrom(x));

var result = new HashSet<Type>();

foreach (var item in dTypes)
result.Add(item);

return result;
}

public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
if (!derivedTypes.Value.Contains(type)) return;

var clonedSchema = new Schema
{
properties = schema.properties,
type = schema.type,
required = schema.required
};

//schemaRegistry.Definitions[typeof(T).Name]; does not work correctly in SwashBuckle
var parentSchema = new Schema { @ref = "#/definitions/" + typeof(T).Name };

schema.allOf = new List<Schema> { parentSchema, clonedSchema };

//reset properties for they are included in allOf, should be null but code does not handle it
schema.properties = new Dictionary<string, Schema>();
}
}

public class PolymorphismDocumentFilter<T> : IDocumentFilter
{
public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, System.Web.Http.Description.IApiExplorer apiExplorer)
{
RegisterSubClasses(schemaRegistry, typeof(T));
}

private static void RegisterSubClasses(SchemaRegistry schemaRegistry, Type abstractType)
{
const string discriminatorName = "discriminator";

var parentSchema = schemaRegistry.Definitions[SchemaIdProvider.GetSchemaId(abstractType)];

//set up a discriminator property (it must be required)
parentSchema.discriminator = discriminatorName;
parentSchema.required = new List<string> { discriminatorName };

if (!parentSchema.properties.ContainsKey(discriminatorName))
parentSchema.properties.Add(discriminatorName, new Schema { type = "string" });

//register all subclasses
var derivedTypes = abstractType.Assembly
.GetTypes()
.Where(x => abstractType != x && abstractType.IsAssignableFrom(x));

foreach (var item in derivedTypes)
schemaRegistry.GetOrRegister(item);
}
}

What the previous code implements is specified here, in the section "Models with Polymorphism Support. It basically produces something like the following:

{
"definitions": {
"Pet": {
"type": "object",
"discriminator": "petType",
"properties": {
"name": {
"type": "string"
},
"petType": {
"type": "string"
}
},
"required": [
"name",
"petType"
]
},
"Cat": {
"description": "A representation of a cat",
"allOf": [
{
"$ref": "#/definitions/Pet"
},
{
"type": "object",
"properties": {
"huntingSkill": {
"type": "string",
"description": "The measured skill for hunting",
"default": "lazy",
"enum": [
"clueless",
"lazy",
"adventurous",
"aggressive"
]
}
},
"required": [
"huntingSkill"
]
}
]
},
"Dog": {
"description": "A representation of a dog",
"allOf": [
{
"$ref": "#/definitions/Pet"
},
{
"type": "object",
"properties": {
"packSize": {
"type": "integer",
"format": "int32",
"description": "the size of the pack the dog is from",
"default": 0,
"minimum": 0
}
},
"required": [
"packSize"
]
}
]
}
}
}

Using Swashbuckle for Asp.net core how can I add a model to the generated model list?

You could create an document filter and register it globally.

public class CustomModelDocumentFilter<T> : IDocumentFilter where T : class
{
public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
context.SchemaRegistry.GetOrRegister(typeof(T));
}
}

and then register it in your Startup class.

services.AddSwaggerGen(options =>
{
...
options.DocumentFilter<CustomModelDocumentFilter<MyCustomModel>>();
options.DocumentFilter<CustomModelDocumentFilter<MyOtherModel>>();
...
}

For polymorphic class you could use these to filters (slightly improved versions of this answer).

public class PolymorphismDocumentFilter<T> : IDocumentFilter
{
public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
RegisterSubClasses(context.SchemaRegistry, typeof(T));
}

private static void RegisterSubClasses(ISchemaRegistry schemaRegistry, Type abstractType)
{
const string discriminatorName = "$type";

string friendlyId = abstractType.FriendlyId();
if (!schemaRegistry.Definitions.TryGetValue(friendlyId, out Schema parentSchema))
parentSchema = schemaRegistry.GetOrRegister(abstractType);

// set up a discriminator property (it must be required)
parentSchema.Discriminator = discriminatorName;
parentSchema.Required = new List<string> { discriminatorName };

if (parentSchema.Properties == null)
parentSchema.Properties = new Dictionary<string, Schema>();

if (!parentSchema.Properties.ContainsKey(discriminatorName))
parentSchema.Properties.Add(discriminatorName, new Schema { Type = "string", Default = abstractType.FullName });

// register all subclasses
var derivedTypes = abstractType.GetTypeInfo().Assembly.GetTypes()
.Where(x => abstractType != x && abstractType.IsAssignableFrom(x));

foreach (var item in derivedTypes)
schemaRegistry.GetOrRegister(item);
}
}

public class PolymorphismSchemaFilter<T> : ISchemaFilter
{
private readonly Lazy<HashSet<Type>> derivedTypes = new Lazy<HashSet<Type>>(Init);

public void Apply(Schema schema, SchemaFilterContext context)
{
if (!derivedTypes.Value.Contains(context.SystemType)) return;

var type = context.SystemType;
var clonedSchema = new Schema
{
Properties = schema.Properties,
Type = schema.Type,
Required = schema.Required
};

// schemaRegistry.Definitions[typeof(T).Name]; does not work correctly in Swashbuckle.AspNetCore
var parentSchema = new Schema { Ref = "#/definitions/" + typeof(T).Name };

var assemblyName = Assembly.GetAssembly(type).GetName();
schema.Discriminator = "$type";
// This is required if you use Microsoft's AutoRest client to generate the JavaScript/TypeScript models
schema.Extensions.Add("x-ms-discriminator-value", $"{type.FullName}, {assemblyName.Name}");
schema.AllOf = new List<Schema> { parentSchema, clonedSchema };

// reset properties for they are included in allOf, should be null but code does not handle it
schema.Properties = new Dictionary<string, Schema>();
}

private static HashSet<Type> Init()
{
var abstractType = typeof(T);
var dTypes = abstractType.GetTypeInfo().Assembly
.GetTypes()
.Where(x => abstractType != x && abstractType.IsAssignableFrom(x));

var result = new HashSet<Type>();

foreach (var item in dTypes)
result.Add(item);

return result;
}
}

Two filters are required. The first will add all of your delivered classes to the schema. It also adds properties that aren't existing int he base class to the derived type's schema.

The second filter adds some properties ($type for serialization when the model returns) and extensions (for Microsoft's AutoRest client / generator) as well as adding the allOf properties to the Swagger schema, which are required in order to create a inheritance schema when generated with swagger-gen or AutoRest.

Registration is similar, just that you need to register them in pairs (only registration of the base class is required)

// The following lines add polymorphism to the swagger.json schema, so that
// code generators can create properly inheritance hierarchies.
options.DocumentFilter<PolymorphismDocumentFilter<BaseClass>>();
options.SchemaFilter<PolymorphismSchemaFilter<BaseClass>>();

Update for ASP.NET Core 3 and Swashbuckle.AspNetCore 5.0

public class CustomModelDocumentFilter<T> : IDocumentFilter where T : class
{
public void Apply(OpenApiDocument openapiDoc, DocumentFilterContext context)
{
context.SchemaGenerator.GenerateSchema(typeof(T), context.SchemaRepository);
}
}

The PolymorphismDocumentFilter/PolymorphismSchemaFilter updated for Swashbuckle.AspNetCore 5.0

public class PolymorphismDocumentFilter<T> : IDocumentFilter
{
public void Apply(OpenApiDocument openApiDoc, DocumentFilterContext context)
{
RegisterSubClasses(context, typeof(T));
}

private static void RegisterSubClasses(DocumentFilterContext context, Type abstractType)
{
const string discriminatorName = "$type";
var schemaRepository = context.SchemaRepository.Schemas;
var schemaGenerator = context.SchemaGenerator;

if (!schemaRepository.TryGetValue(abstractType.Name, out OpenApiSchema parentSchema))
{
parentSchema = schemaGenerator.GenerateSchema(abstractType, context.SchemaRepository);
}

// set up a discriminator property (it must be required)
parentSchema.Discriminator = new OpenApiDiscriminator { PropertyName = discriminatorName };
parentSchema.Required.Add(discriminatorName);

if (!parentSchema.Properties.ContainsKey(discriminatorName))
parentSchema.Properties.Add(discriminatorName, new OpenApiSchema { Type = "string", Default = new OpenApiString(abstractType.FullName) });

// register all subclasses
var derivedTypes = abstractType.GetTypeInfo().Assembly.GetTypes()
.Where(x => abstractType != x && abstractType.IsAssignableFrom(x));

foreach (var type in derivedTypes)
schemaGenerator.GenerateSchema(type, context.SchemaRepository);
}
}

and

public class PolymorphismSchemaFilter<T> : ISchemaFilter
{
private readonly Lazy<HashSet<Type>> derivedTypes = new Lazy<HashSet<Type>>(Init);

public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var type = context.ApiModel.Type;
if (!derivedTypes.Value.Contains(type))
return;

var clonedSchema = new OpenApiSchema
{
Properties = schema.Properties,
Type = schema.Type,
Required = schema.Required
};

// schemaRegistry.Definitions[typeof(T).Name]; does not work correctly in SwashBuckle
if(context.SchemaRepository.Schemas.TryGetValue(typeof(T).Name, out OpenApiSchema _))
{
schema.AllOf = new List<OpenApiSchema> {
new OpenApiSchema { Reference = new OpenApiReference { Id = typeof(T).Name, Type = ReferenceType.Schema } },
clonedSchema
};
}

var assemblyName = Assembly.GetAssembly(type).GetName();
schema.Discriminator = new OpenApiDiscriminator { PropertyName = "$type" };
schema.AddExtension("x-ms-discriminator-value", new OpenApiString($"{type.FullName}, {assemblyName.Name}"));

// reset properties for they are included in allOf, should be null but code does not handle it
schema.Properties = new Dictionary<string, OpenApiSchema>();
}

private static HashSet<Type> Init()
{
var abstractType = typeof(T);
var dTypes = abstractType.GetTypeInfo().Assembly
.GetTypes()
.Where(x => abstractType != x && abstractType.IsAssignableFrom(x));

var result = new HashSet<Type>();

foreach (var item in dTypes)
result.Add(item);

return result;
}
}

Swashbuckle ISchemaFilter based on method

You need to use OperationFilter. you can use it like below.

    public class CustomOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (operation.OperationId == "Bark")
{
operation.RequestBody.Content["application/json"].Example =
OpenApiAnyFactory.CreateFromJson("{\"GoodBoiBarksThisLoud\":0}");
}
if (operation.OperationId == "ShowMeThoseLegs")
{
operation.RequestBody.Content["application/json"].Example =
OpenApiAnyFactory.CreateFromJson("{\"Legs\":0}");
}
}
}

and in Program.cs

builder.Services.AddSwaggerGen(
o => {
o.CustomOperationIds(e => $"{e.ActionDescriptor.RouteValues["action"]}");
o.OperationFilter<CustomOperationFilter>();
});

then you will see swagger ui something like this.

Sample Image

Hope this helps.

Creating swagger example models asp.net core web api

I found the answer here for the latest package

https://github.com/domaindrivendev/Swashbuckle.AspNetCore#enrich-schema-metadata

Include XML Comments for enums in Swagger and ReDoc based OpenAPI documentation

I found an issue on GitHub that asks for this as a feature request.

To summarize that thread, OP there requests the same feature as described in the question. Later, two possibilities are suggested:

  1. Find a place in the swagger spec where these docs belong
  2. Have Swashbuckle do some string concatenation and add the enum descriptions to appropriate places (e.g. properties that use the enum)

The first option turned out impossible (no such place in the spec). The second option was rejected.

So, in short: what you want is not possible.



Related Topics



Leave a reply



Submit