How to include subclasses in the Swagger API documentation / OpenAPI specification using Swashbuckle?

I have an Asp.Net web API 5.2 project in C # and creating documentation using Swashbuckle.

I have a model that inherits something like the presence of the Animal property from the abstract Animal class and the Dog and Cat classes that derive from it.

Swashbuckle only shows the diagram for the Animal class, so I tried playing with ISchemaFilter (this is what they offer), but I could not get it to work, and I also could not find a suitable example.

Can anybody help?

+14
c # api subclassing swagger swashbuckle openapi
source share
4 answers

It seems that Swashbuckle does not implement polymorphism correctly, and I understand the authorโ€™s point of view regarding subclasses as parameters (if the action expects the Animal class and behaves differently, if you call it using the dog object or cat object, then you should have 2 different actions ...) but as return types I consider it correct to return Animal and objects can be of type Dog or Cat.

Therefore, to describe my API and create the correct JSON scheme in accordance with the correct recommendations (remember how I describe the discriminator, if you have your own discriminator, you may need to change this part in particular), I use document and scheme filters following:

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 implements the previous code is indicated here in the section "Models with support for polymorphism." Essentially, it 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" ] } ] } } } 
+23
source share

To continue with Paulo's excellent answer, if you are using Swagger 2.0, you need to change the classes as shown:

 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 model, SchemaFilterContext context) { if (!derivedTypes.Value.Contains(context.SystemType)) return; var clonedSchema = new Schema { Properties = model.Properties, Type = model.Type, Required = model.Required }; //schemaRegistry.Definitions[typeof(T).Name]; does not work correctly in SwashBuckle var parentSchema = new Schema { Ref = "#/definitions/" + typeof(T).Name }; model.AllOf = new List<Schema> { parentSchema, clonedSchema }; //reset properties for they are included in allOf, should be null but code does not handle it model.Properties = new Dictionary<string, Schema>(); } } public class PolymorphismDocumentFilter<T> : IDocumentFilter { private static void RegisterSubClasses(ISchemaRegistry schemaRegistry, Type abstractType) { const string discriminatorName = "discriminator"; var parentSchema = schemaRegistry.Definitions[abstractType.Name]; //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); } public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context) { RegisterSubClasses(context.SchemaRegistry, typeof(T)); } } 
+10
source share

I would like to continue Craig's answer.

If you use NSwag to generate TypeScript definitions from the Swagger API documentation generated using Swashbuckle (3.x at the time of writing) using the method described in Paulo's answer and enhanced in Craig's answer, you will probably encounter the following problems:

  1. Generated TypeScript definitions will have duplicate properties, even if the generated classes extend the base classes. Consider the following C # classes:

     public abstract class BaseClass { public string BaseProperty { get; set; } } public class ChildClass : BaseClass { public string ChildProperty { get; set; } } 

    Using the above answers, the resulting TypeScript definition of the IBaseClass and IChildClass will look like this:

     export interface IBaseClass { baseProperty : string | undefined; } export interface IChildClass extends IBaseClass { baseProperty : string | undefined; childProperty: string | undefined; } 

    As you can see, baseProperty incorrectly defined in both base and child classes. To solve this problem, we can change the Apply method of the PolymorphismSchemaFilter<T> class to include only its own properties in the scheme, i.e. Exclude inherited properties from the current type schema. Here is an example:

     public void Apply(Schema model, SchemaFilterContext context) { ... // Prepare a dictionary of inherited properties var inheritedProperties = context.SystemType.GetProperties() .Where(x => x.DeclaringType != context.SystemType) .ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase); var clonedSchema = new Schema { // Exclude inherited properties. If not excluded, // they would have appeared twice in nswag-generated typescript definition Properties = model.Properties.Where(x => !inheritedProperties.ContainsKey(x.Key)) .ToDictionary(x => x.Key, x => x.Value), Type = model.Type, Required = model.Required }; ... } 
  2. Generated TypeScript definitions will not reference the properties of any existing intermediate abstract classes. Consider the following C # classes:

     public abstract class SuperClass { public string SuperProperty { get; set; } } public abstract class IntermediateClass : SuperClass { public string IntermediateProperty { get; set; } } public class ChildClass : BaseClass { public string ChildProperty { get; set; } } 

    In this case, the generated TypeScript definitions will look like this:

     export interface ISuperClass { superProperty: string | undefined; } export interface IIntermediateClass extends ISuperClass { intermediateProperty : string | undefined; } export interface IChildClass extends ISuperClass { childProperty: string | undefined; } 

    Pay attention to how the generated IChildClass interface extends ISuperClass directly, ignoring the IIntermediateClass interface, effectively leaving an instance of IChildClass without intermediateProperty property.

    We can use the following code to solve this problem:

     public void Apply(Schema model, SchemaFilterContext context) { ... // Use the BaseType name for parentSchema instead of typeof(T), // because we could have more classes in the hierarchy var parentSchema = new Schema { Ref = "#/definitions/" + (context.SystemType.BaseType?.Name ?? typeof(T).Name) }; ... } 

    This ensures that the child class references the intermediate class correctly.

In conclusion, the resulting code will look like this:

  public void Apply(Schema model, SchemaFilterContext context) { if (!derivedTypes.Value.Contains(context.SystemType)) { return; } // Prepare a dictionary of inherited properties var inheritedProperties = context.SystemType.GetProperties() .Where(x => x.DeclaringType != context.SystemType) .ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase); var clonedSchema = new Schema { // Exclude inherited properties. If not excluded, // they would have appeared twice in nswag-generated typescript definition Properties = model.Properties.Where(x => !inheritedProperties.ContainsKey(x.Key)) .ToDictionary(x => x.Key, x => x.Value), Type = model.Type, Required = model.Required }; // Use the BaseType name for parentSchema instead of typeof(T), // because we could have more abstract classes in the hierarchy var parentSchema = new Schema { Ref = "#/definitions/" + (context.SystemType.BaseType?.Name ?? typeof(T).Name) }; model.AllOf = new List<Schema> { parentSchema, clonedSchema }; // reset properties for they are included in allOf, should be null but code does not handle it model.Properties = new Dictionary<string, Schema>(); } 
+3
source share

have an Asp.Net web API 5.2 project in C # and generate documentation using Swashbuckle.

In our method, we return Error and BusinessIntelligenceItemDTO List from the Get method, and error also from the same method.

Swashbuckle only shows the schema for [ResponseType (typeof (BusinessIntelligenceItemDTO))]], but not for the error class

Can anybody help?

0
source share

All Articles