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
会:
- 扫描所有带
[JsonPolymorphic]的基类 - 读取
[JsonDerivedType]列表 - 自动生成每个子类的 OpenAPI Schema
- 把基类 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 多态文档分两层解决:
- 运行时序列化层:
System.Text.Json的[JsonPolymorphic]+[JsonDerivedType],让 API 能正确收发多态 JSON - Swagger 文档层:Swashbuckle 的
UseOneOfForPolymorphism(),自动把多态翻译成 OpenAPIoneOfSchema
两层都配好之后,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