如何使用Swashbuckle在Swagger API文档/OpenAPI规范中包含子类?

43
我有一个使用C#编写的Asp.Net Web API 5.2项目,使用Swashbuckle生成文档。
我的模型中包含继承,例如从Animal抽象类派生出Dog和Cat类,并拥有Animal属性。
然而,Swashbuckle只显示Animal类的模式。我尝试了ISchemaFilter(这也是他们建议的方法),但我无法使其正常工作,也找不到适当的示例。
有人能提供帮助吗?

从今天开始,您应该考虑更新您的包。在我的情况下,我更新了最新的NSwag.AspNetCore -版本13.1.6(而不是11.18.7),解决了这个问题。 - Bellash
这在过去几年发生了很大变化,Swashbuckle现在确实实现了多态性。 - Jaap
6个回答

31
似乎Swashbuckle没有正确实现多态性,我理解作者关于子类作为参数的观点(如果一个操作期望Animal类,并且在调用它时使用dog对象或cat对象会有不同的行为,则应该有2个不同的操作...),但是对于返回类型,我认为返回Animal是正确的,而对象可以是Dog或Cat类型。
因此,为了描述我的API并生成符合正确指南的正确JSON模式(请注意我如何描述鉴别器,如果您有自己的鉴别器,则可能需要特别更改该部分),我使用文档和模式过滤器如下:
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);
    }
}

上述代码实现的内容在这里被指定, 在“具有多态支持的模型”一节中。它基本上会产生以下结果:

{
  "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"
          ]
        }
      ]
    }
  }
}

2
SchemaIdProvider 必须是你自己的类吗?我发现你可以通过添加一个 Using Swashbuckle.Swagger 来使用 Swagger 的默认约定,然后将那行代码改为 var parentSchema = schemaRegistry.Definitions[abstractType.FriendlyId]; - wags1999
是的,这是我的类。我需要它,因为我们还有一个用于schemaId的委托:configuration.SchemaId(SchemaIdProvider.GetSchemaId); - Paolo Vigori
2
@PaoloVigori:我在Swashbuckle.AspNetCore上使用了它,PolymorphismDocumentFilter已被调用并且鉴别器已在代码中设置,但未在生成的Swagger定义中设置。 allOf 条目已经存在。有什么想法吗? - Tseng
克隆的模式也不应该复制 model.AllOf 吗?否则,从派生类型派生的类型将没有任何属性。 - g t

16

继Paulo的精彩回答之后,如果你正在使用Swagger 2.0,你需要修改如下类:

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

16

自从将 此次合并 到 Swashbuckle.AspNetCore 中后,您可以通过使用以下方式获得对多态模式的基本支持:

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

您也可以通过注释库中存在的属性来表示派生类型:

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

本文详细介绍了如何使用Newtonsoft进行派生类型的反序列化。


你的“type”属性的名称是鉴别器吗? - Andra Avram
@AndraAvram 这是用于区分的属性名称。在这个示例中,instrumentType是鉴别器。 - Strake
1
这是我认为最好的答案。使继承变得非常简单。此外,SwaggerSubTypes现在已经过时,取而代之的是为每种类型使用单独的SwaggerSubType属性。 - Jace Rhea

10

截至5.6.3版本,此方法可行:

services.AddSwaggerGen(options =>
{
    options.UseOneOfForPolymorphism();
    options.SelectDiscriminatorNameUsing(_ => "type");
});  

10

我想跟进一下Craig的答案。

如果您使用NSwag从使用Swashbuckle(在编写时为3.x版本)生成的Swagger API文档中生成TypeScript定义,可以使用Paulo的答案中所述的方法,并通过Craig的答案进一步改进。您可能会遇到以下问题:

  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.

总之,最终代码将如下所示:
    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>();
    }

太好了 - 正是我需要的。非常感谢你的分享! - Matthias
接下来我遇到了错误:由于无法解析指针:/definitions/SuperClass在文档中不存在,因此无法解析引用。请问有什么方法可以修复它吗? - Sandy
@Sandy 在配置模式过滤器时是否引用了 SuperClass,例如 configuration.SchemaFilter<PolymorphismSchemaFilter<SuperClass>>(); - Dejan Janjušević
@DejanJanjušević:是的,那就是问题所在,我在配置时没有引用SuperClass。感谢您的帮助。 - Sandy

9
我们最近升级了.NET Core 3.1和Swashbuckle.AspNetCore 5.0,并且API有所更改。如果有人需要这个过滤器,则以下代码能够实现相似的功能,只需进行最小化的更改:
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;
    }
}

我没有完全检查结果,但似乎它提供了相同的行为。

还要注意,您需要导入以下命名空间:

using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Any;
using System.Reflection;
using Swashbuckle.AspNetCore.SwaggerGen;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接