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

6

44

I have an Asp.Net web API 5.2 project in c# and generating documentation with Swashbuckle.

I have model that contain inheritance something like having an Animal property from an Animal abstract class and Dog and Cat classes that derive from it.

Swashbuckle only shows the schema for the Animal class so I tried to play with ISchemaFilter (that what they suggest too) but I couldn't make it work and also I cannot find a proper example.

Anybody can help?

Footplate answered 21/12, 2015 at 13:55 Comment(2)
As of today, you should consider updating your package. In my case, I updated the latest NSwag.AspNetCore -Version 13.1.6 (instead of 11.18.7) solve this issue.Nickolas
This changed a lot over the last few years, Swashbuckle does implement polymorphism now.Belleslettres
F
32

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"
          ]
        }
      ]
    }
  }
}
Footplate answered 24/3, 2016 at 12:52 Comment(5)
SchemaIdProvider must be your own class? I figured out you can use Swagger's default convention by adding a Using Swashbuckle.Swagger and then changing that line of code to var parentSchema = schemaRegistry.Definitions[abstractType.FriendlyId];Wrapping
Yes, it's my class. I need it because also we have a delegate for schemaId: configuration.SchemaId(SchemaIdProvider.GetSchemaId);Footplate
@PaoloVigori: I used that on Swashbuckle.AspNetCore, the PolymorphismDocumentFilter is called and discriminator is set in the code, but not in the generated swagger definition. allOf entries are there. Any ideas?Standish
Shouldn't the cloned schema also copy model.AllOf? Otherwise types which derive from derived types will not have any properties.Scent
Swashbuckle supports this now; see this answer: https://mcmap.net/q/375196/-swashbuckle-polymorphism-not-working-with-external-nuget-packageThormora
B
19

As of this merge into Swashbuckle.AspNetCore, you can get basic support for polymorphic schemas by using:

services.AddSwaggerGen(c =>
{
    c.GeneratePolymorphicSchemas();
}

You can also express your derived types via attributes present in the Annotations library:

[SwaggerSubTypes(typeof(SubClass), Discriminator = "value")]

This article goes into further detail as to how you can deserialize derived types using Newtonsoft.

Bostow answered 22/5, 2020 at 20:43 Comment(3)
Is the discriminator the name of your "type" attribute?Rosalindrosalinda
@AndraAvram It is the name of the property to discriminate on. In the example here, instrumentType is the discriminator.Bostow
This is the best answer, IMO. Made inheritance very simple. Also, SwaggerSubTypes is now obsolete in favor of individual SwaggerSubType attributes for each type.Kraul
I
16

To follow on from Paulo's great answer, if you're using Swagger 2.0, you'll need to modify 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));
    }
}
Instep answered 30/3, 2018 at 15:9 Comment(0)
I
11

This works as of version 5.6.3:

services.AddSwaggerGen(options =>
{
    options.UseOneOfForPolymorphism();
    options.SelectDiscriminatorNameUsing(_ => "type");
});  
Inexpressible answered 10/2, 2021 at 19:24 Comment(0)
N
10

I'd like to follow up on Craig's answer.

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

  1. Generated TypeScript definitions will have duplicate properties even though the generated classes will 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; }
    }
    

    When using the aforementioned answers, the resulting TypeScript definition of IBaseClass and IChildClass interfaces would look like this:

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

    As you can see, the baseProperty is incorrectly defined in both base and child classes. To solve this, we can modify the Apply method of the PolymorphismSchemaFilter<T> class to include only owned properties to the schema, i.e. to exclude the inherited properties from the current types 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 properties from 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 would 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;
    }
    

    Notice how the generated IChildClass interface extends ISuperClass directly, ignoring the IIntermediateClass interface, effectively leaving any instance of IChildClass without the 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 will ensure that the child class correctly references the intermediate class.

In conclusion, the final code would then 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>();
    }
Nonresistant answered 5/1, 2019 at 14:30 Comment(4)
Great - just what I needed. Thank you very much for sharing!Larval
Following this I get error Could not resolve reference because of: Could not resolve pointer: /definitions/SuperClass does not exist in document, any pointers to fix itGeomancer
@Geomancer have you referenced the SuperClass when configuring the schema filter, i.e. configuration.SchemaFilter<PolymorphismSchemaFilter<SuperClass>>(); ?Aulea
@DejanJanjušević: Yes that was the issue, I was not referring to SuperClass while configuring. Thanks for helping out.Geomancer
S
9

We recently upgraded to .NET Core 3.1 and Swashbuckle.AspNetCore 5.0 And the API is somewhat changed. In case somebody needs this filter here is the code with minimal changes to get similar behavior:

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

    private static void RegisterSubClasses(SchemaRepository schemaRegistry, ISchemaGenerator schemaGenerator, Type abstractType)
    {
        const string discriminatorName = "$type";
        OpenApiSchema parentSchema = null;

        if (schemaRegistry.TryGetIdFor(abstractType, out string parentSchemaId))
            parentSchema = schemaRegistry.Schemas[parentSchemaId];
        else
            parentSchema = schemaRegistry.GetOrAdd(abstractType, parentSchemaId, () => new OpenApiSchema());

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

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

        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 item in derivedTypes)
            schemaGenerator.GenerateSchema(item, schemaRegistry);
    }
}

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

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

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

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

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

        // 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;
    }
}

I did not inspect the result fully, but it seems that it gives the same behavior.

Also note that you need to import these namespaces:

using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Any;
using System.Reflection;
using Swashbuckle.AspNetCore.SwaggerGen;
Smetana answered 5/3, 2020 at 15:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.