1. 问题背景:Swagger 只展示了基类

在 Web API 项目中,我们经常会设计一个基类 + 多个子类的继承体系,用一个接口同时处理多种请求。比如 SwitchData 项目里的 Excel / TXT / Database 三种导入方式:

public class ReviewBase
{
    public string FileName { get; set; }
}

public class ExcelReviewRequest : ReviewBase
{
    public string SheetName { get; set; }
    public string LeftTopRow { get; set; }
    public string RightLowRow { get; set; }
    public bool IncludeTitle { get; set; }
    public bool SplitMergedCells { get; set; }
}

public class TxtReviewRequest : ReviewBase
{
    public TxtReviewTypeEnum TxtReviewType { get; set; }
    public bool IncludeTitle { get; set; }
    public char[] Separators { get; set; }
    public string Encoder { get; set; }
}

public class DatabaseReviewRequest : ReviewBase
{
    public string TableName { get; set; }
    public string[] QueryColumns { get; set; }
}

然后接口参数用基类:

[HttpPost("review")]
public IActionResult Review([FromBody] ReviewBase request)
{
    // 运行时根据 request 的 fileType 标识判断具体类型
}

但 Swagger(Swashbuckle)默认只认识基类 ReviewBase,生成的 Schema 里只有 FileName 一个字段。前端开发者打开 Swagger UI,完全看不到 ExcelReviewRequest 有 SheetName,TxtReviewRequest 有 Separators —— 这就失去了文档的意义。

本文带你完整解决这个问题,从”手动注册 Schema”到”自动生成 oneOf 引用”。

2. 第一步:让 JSON 序列化器先支持多态

Swagger 文档本质上是把模型类型翻译成 OpenAPI Schema。如果 JSON 序列化器本身不支持多态,Swashbuckle 也不可能正确展示。

2.1 System.Text.Json 的 JsonPolymorphic

.NET 7 引入了全新的 [JsonPolymorphic] + [JsonDerivedType] 特性,让 System.Text.Json 在运行时根据类型标识字段(type discriminator)自动选择正确的子类进行序列化/反序列化:

using System.Text.Json.Serialization;

// 基类标记多态入口,用 "fileType" 字段作为类型标识
[JsonPolymorphic(TypeDiscriminatorPropertyName = "fileType")]
[JsonDerivedType(typeof(ExcelReviewRequest), typeDiscriminator: "excel")]
[JsonDerivedType(typeof(TxtReviewRequest), typeDiscriminator: "txt")]
[JsonDerivedType(typeof(DatabaseReviewRequest), typeDiscriminator: "database")]
public class ReviewBase : PageRequest
{
    public string FileName { get; set; }
}

这样当 API 收到 JSON 时:

{
  "fileType": "excel",
  "fileName": "2024-订单.xlsx",
  "sheetName": "Sheet1",
  "leftTopRow": "A2",
  "rightLowRow": "F100"
}

System.Text.Json 会看到 fileType = "excel",自动反序列化为 ExcelReviewRequest。

注意:Newtonsoft.Json 也支持多态(TypeNameHandling.Auto),但它的做法是在 $type 里嵌入 CLR 类型全名,既不安全(暴露内部结构)又冗长。JsonPolymorphic 用的是业务友好的简短标识,推荐新项目全部迁移。

3. 第二步:让 Swashbuckle 把多态翻译成 OpenAPI oneOf

JsonPolymorphic 解决了运行时问题,但 Swashbuckle 还不知道 ReviewBase 有三个子类。我们需要告诉 Swagger 生成器,把 ReviewBase 的 Schema 改成 oneOf 引用三个子类。

3.1 方案 A:手动 DocumentFilter(SwitchData 现有方案)

SwitchData 项目里用了一个极简的 IDocumentFilter:

public class PolymorphismDocumentFilter : IDocumentFilter
{
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        context.SchemaGenerator.GenerateSchema(typeof(ExcelReviewRequest), context.SchemaRepository);
        context.SchemaGenerator.GenerateSchema(typeof(TxtReviewRequest), context.SchemaRepository);
        context.SchemaGenerator.GenerateSchema(typeof(DatabaseReviewRequest), context.SchemaRepository);
    }
}

然后在 Program.cs 里注册:

services.AddSwaggerGen(options =>
{
    // ...其他配置...
    options.DocumentFilter<PolymorphismDocumentFilter>();
    options.CustomSchemaIds(type => type.FullName);
});

这段代码的作用是:强制 Swashbuckle 提前为三个子类生成 OpenAPI Schema 并注册到 SchemaRepository。这样即使 ReviewBase 本身没有直接引用子类,三个子类的 Schema 也会出现在 Swagger UI 的 Models 区域,前端可以手动点开查看。

问题:它只是把 Schema 注册进去了,但 ReviewBase 的 Schema 仍然只展示基类自己的字段,没有变成 oneOf 引用。用户必须自己去 Models 里找子类,不是最完美的体验。

3.2 方案 B:自动 oneOf(推荐方案)

Swashbuckle 本身是认识 [JsonPolymorphic] 的 —— 只要开启一个选项。

services.AddSwaggerGen(options =>
{
    // 开启对 JsonPolymorphic 特性的自动识别
    options.UseOneOfForPolymorphism();

    // 可选:用自定义 SchemaId 避免同名冲突
    options.CustomSchemaIds(type => type.FullName);
});

开启 UseOneOfForPolymorphism() 之后,Swashbuckle 会:

  1. 扫描所有带 [JsonPolymorphic] 的基类
  2. 读取 [JsonDerivedType] 列表
  3. 自动生成每个子类的 OpenAPI Schema
  4. 把基类 Schema 改成 oneOf 引用所有子类

生成的 OpenAPI JSON 大致是这样:

"schemas": {
  "ExcelReviewRequest": {
    "type": "object",
    "allOf": [{ "$ref": "#/components/schemas/ReviewBase" }],
    "properties": {
      "sheetName": { "type": "string" },
      "leftTopRow": { "type": "string" },
      "rightLowRow": { "type": "string" },
      "includeTitle": { "type": "boolean" },
      "splitMergedCells": { "type": "boolean" }
    }
  },
  "ReviewBase": {
    "oneOf": [
      { "$ref": "#/components/schemas/ExcelReviewRequest" },
      { "$ref": "#/components/schemas/TxtReviewRequest" },
      { "$ref": "#/components/schemas/DatabaseReviewRequest" }
    ]
  }
}

Swagger UI 渲染 ReviewBase 时,会直接展示一个下拉选择框,让用户选 Excel / TXT / Database 三种请求体模板,每种模板自动填充对应字段 —— 这就是我们想要的效果!

3.3 方案 C:自定义 IOperationFilter 精细控制

有时候我们不想让 Swashbuckle 全自动,需要对特定接口做定制。比如有些接口确实只用 ReviewBase(纯基类),有些接口要展示子类。这时候可以写一个 IOperationFilter:

public class PolymorphismOperationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        if (!operation.RequestBody?.Content.ContainsKey("application/json") == false) return;

        var mediaType = operation.RequestBody.Content["application/json"];
        var schema = mediaType.Schema;

        // 如果已经是 oneOf 了就跳过
        if (schema is OpenApiReference reference
            && reference.Type == ReferenceType.Schema)
        {
            var targetName = reference.Id;
            var baseType = context.SchemaRepository.Schemas.GetValueOrDefault(targetName);

            if (baseType != null && baseType.OneOf == null)
            {
                baseType.OneOf = new List<OpenApiSchema>();
                baseType.OneOf.Add(new OpenApiReference
                    { Type = ReferenceType.Schema, Id = "ExcelReviewRequest" });
                baseType.OneOf.Add(new OpenApiReference
                    { Type = ReferenceType.Schema, Id = "TxtReviewRequest" });
                baseType.OneOf.Add(new OpenApiReference
                    { Type = ReferenceType.Schema, Id = "DatabaseReviewRequest" });
            }
        }
    }
}

然后注册:

options.OperationFilter<PolymorphismOperationFilter>();

4. 完整配置清单

结合 SwitchData 项目的实际代码,一个生产级的 Swagger 多态配置应该是:

private static void ConfigureSwagger(IServiceCollection services)
{
    services.AddEndpointsApiExplorer();
    services.AddSwaggerGen(options =>
    {
        options.SwaggerDoc("v1", new OpenApiInfo
        {
            Title = "SwitchData Service API",
            Version = "v1"
        });

        // ① 自动识别 JsonPolymorphic,生成 oneOf
        options.UseOneOfForPolymorphism();

        // ② 手动 DocumentFilter 兜底(兼容未用特性标记的旧类型)
        options.DocumentFilter<PolymorphismDocumentFilter>();

        // ③ 自定义 SchemaId,防止不同命名空间同名类冲突
        options.CustomSchemaIds(type => type.FullName);

        // ④ 包含 XML 文档注释
        var xmlFile = Assembly.GetExecutingAssembly().GetName().Name + ".xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true);

        // ⑤ 启用 Swagger Annotations 特性
        options.EnableAnnotations();

        // ⑥ JWT 安全配置
        options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
        {
            Name = "Authorization",
            Type = SecuritySchemeType.ApiKey,
            Scheme = "Bearer",
            BearerFormat = "JWT",
            In = ParameterLocation.Header,
        });
        options.AddSecurityRequirement(new OpenApiSecurityRequirement
        {
            {
                new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Bearer"
                    }
                },
                Array.Empty<string>()
            }
        });
    });
}

5. 踩过的坑

5.1 CustomSchemaIds 必须配置

Swashbuckle 默认用类型的 Name(不是 FullName)作为 SchemaId。如果不同命名空间下有同名类(比如 Models.Request 和 Dtos.Request),会导致 SchemaId 冲突。务必加上:

options.CustomSchemaIds(type => type.FullName);

5.2 JsonPolymorphic 的 type discriminator 必须参与序列化

[JsonPolymorphic(TypeDiscriminatorPropertyName = "fileType")] 里的 fileType 不是自动生成的,你需要在基类或 JSON 里手动提供。SwitchData 项目里是通过客户端请求体带的。如果你希望基类自动带上,可以:

[JsonPolymorphic(TypeDiscriminatorPropertyName = "fileType",
    UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]
[JsonDerivedType(typeof(ExcelReviewRequest), "excel")]
[JsonDerivedType(typeof(TxtReviewRequest), "txt")]
[JsonDerivedType(typeof(DatabaseReviewRequest), "database")]
public class ReviewBase
{
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public string FileType { get; set; }
}

5.3 UnknownDerivedTypeHandling 的选择

  • JsonUnknownDerivedTypeHandling.FailSerialization:遇到未注册的子类直接抛异常(推荐开发环境)
  • JsonUnknownDerivedTypeHandling.JsonObject:退化成普通 JObject(生产环境兜底)

5.4 IncludeXmlComments 必须开启

Swashbuckle 的 XML 注释是文档的灵魂。确保 .csproj 里开启了 XML 文档生成:

<PropertyGroup>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <NoWarn>1701;1702</NoWarn>
</PropertyGroup>

6. 三种方案对比

维度 手动 DocumentFilter UseOneOfForPolymorphism 自定义 OperationFilter
配置量 需手写每个子类 GenerateSchema 1 行代码自动识别 按需精确控制
Schema 引用 只注册,不建立 oneOf 自动生成 oneOf/allOf 手动 oneOf
扩展性 新增子类必须改代码 新增子类只需加特性 灵活但易漏
Swagger UI 效果 子类出现在 Models 列表 接口参数直接展示下拉 可控
适用场景 .NET 6 及更早 .NET 7+ 推荐 复杂混合场景

推荐:新项目一律用 UseOneOfForPolymorphism() + JsonPolymorphic。如果因为历史原因用不了(比如还在 .NET 6 或 Newtonsoft.Json),再退而用手动 DocumentFilter。

7. 总结

Swagger 多态文档分两层解决:

  1. 运行时序列化层:System.Text.Json 的 [JsonPolymorphic] + [JsonDerivedType],让 API 能正确收发多态 JSON
  2. Swagger 文档层:Swashbuckle 的 UseOneOfForPolymorphism(),自动把多态翻译成 OpenAPI oneOf Schema

两层都配好之后,Swagger UI 会: - 基类 Schema 变成 oneOf 引用所有子类 - 请求体编辑器展示子类下拉选择 - 每个子类的独有字段一目了然

SwitchData 项目里已经迈出了第一步(PolymorphismDocumentFilter + JsonPolymorphic),还差一步开启 UseOneOfForPolymorphism() 就能享受到完整的自动 oneOf 体验。


Mermaid 流程图:多态 Swagger 生成流程

flowchart TD
    A[Controller 接口 Review Request] --> B{Swashbuckle 扫描参数类型}
    B --> C{ReviewBase 是否有 JsonPolymorphic}
    C -->|是| D[读取 JsonDerivedType 列表]
    D --> E[GenerateSchema 每个子类]
    E --> F[构建 oneOf 引用]
    F --> G[写入 OpenAPI Document]
    G --> H[Swagger UI 渲染子类下拉]

    C -->|否| I[只 GenerateSchema 基类]
    I --> J[DocumentFilter 兜底]
    J --> K[手动 Register 子类 Schema]
    K --> H

    style H fill:#4CAF50,stroke:#2E7D32,color:white
    style D fill:#81C784,stroke:#388E3C
    style F fill:#A5D6A7,stroke:#43A047