.NET 7 C#多态JSON反序列化:鉴别器前置问题的规避方案?
.NET 7多态JSON序列化类型鉴别器位置问题的解决方案
问题描述
在构建ASP.NET Core Web API时,使用.NET 7引入的基于类型鉴别器的多态JSON(反)序列化功能,遇到核心限制:该机制强制要求类型鉴别器必须位于JSON对象的首位,这与JSON规范中"对象为无序键值对"的定义冲突。需要找到可行的规避方案,确保类型鉴别器与$id、$ref等元数据属性归为一组放在JSON开头,同时兼容任意键位顺序的请求JSON。
可行规避方案
1. 自定义请求体预处理中间件
通过中间件拦截JSON请求,将指定的元数据键(类型鉴别器、$id、$ref等)移至JSON对象的最前端,再将处理后的请求体传递给后续的反序列化流程。这种方式通用性强,无需修改业务代码。
中间件实现代码
public class JsonMetadataKeyReorderMiddleware { private readonly RequestDelegate _next; private readonly JsonSerializerOptions _jsonOptions; // 可根据自定义配置修改优先级键列表 private readonly string[] _priorityMetadataKeys = new[] { "$type", "$id", "$ref" }; public JsonMetadataKeyReorderMiddleware(RequestDelegate next, JsonSerializerOptions jsonOptions) { _next = next; _jsonOptions = jsonOptions; } public async Task InvokeAsync(HttpContext context) { // 仅处理JSON类型的POST/PUT/PATCH请求 if (context.Request.ContentType?.Contains("application/json") == true && context.Request.Method is "POST" or "PUT" or "PATCH") { // 开启请求体缓冲,允许多次读取 context.Request.EnableBuffering(); using var reader = new StreamReader(context.Request.Body, leaveOpen: true); var rawJson = await reader.ReadToEndAsync(); context.Request.Body.Position = 0; // 解析JSON并重新排序键位 using var jsonDoc = JsonDocument.Parse(rawJson); var rootElement = jsonDoc.RootElement; if (rootElement.ValueKind == JsonValueKind.Object) { var reorderedJsonObject = new JsonObject(); // 先添加优先级元数据键 foreach (var key in _priorityMetadataKeys) { if (rootElement.TryGetProperty(key, out var property)) { reorderedJsonObject.Add(key, property.Clone()); } } // 添加剩余的非元数据键 foreach (var property in rootElement.EnumerateObject()) { if (!_priorityMetadataKeys.Contains(property.Name)) { reorderedJsonObject.Add(property.Name, property.Value.Clone()); } } // 将处理后的JSON写入新的请求流 var processedJson = JsonSerializer.Serialize(reorderedJsonObject, _jsonOptions); var modifiedStream = new MemoryStream(Encoding.UTF8.GetBytes(processedJson)); context.Request.Body = modifiedStream; context.Request.ContentLength = modifiedStream.Length; } } await _next(context); } } // 中间件扩展注册方法 public static class JsonMetadataKeyReorderExtensions { public static IApplicationBuilder UseJsonMetadataKeyReorder(this IApplicationBuilder builder) { return builder.UseMiddleware<JsonMetadataKeyReorderMiddleware>( builder.ApplicationServices.GetRequiredService<JsonSerializerOptions>()); } }
中间件注册方式
在Program.cs中,将中间件注册在路由配置之前,确保请求体先被预处理:
var builder = WebApplication.CreateBuilder(args); // 配置多态序列化规则 builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver { Modifiers = { typeInfo => { if (typeInfo.Kind == JsonTypeInfoKind.Object && typeInfo.Type.IsAssignableTo(typeof(YourBasePolymorphicType))) { typeInfo.PolymorphismOptions = new JsonPolymorphismOptions { TypeDiscriminatorPropertyName = "$type", // 可自定义鉴别器名称 IgnoreUnrecognizedTypeDiscriminators = true, DerivedTypes = { new JsonDerivedType(typeof(YourDerivedType1), nameof(YourDerivedType1)), new JsonDerivedType(typeof(YourDerivedType2), nameof(YourDerivedType2)) } }; } } } }; }); var app = builder.Build(); // 注册元数据键重排中间件 app.UseJsonMetadataKeyReorder(); app.UseRouting(); app.UseAuthorization(); app.MapControllers(); app.Run();
2. 自定义JsonConverter(备选方案)
如果不需要全局处理,可针对特定多态基类实现自定义JsonConverter,在反序列化阶段手动解析JSON并重排键位。这种方式灵活性高,但需要为每个多态基类单独实现:
public class PolymorphicTypeConverter<TBase> : JsonConverter<TBase> { private readonly JsonSerializerOptions _innerOptions; private readonly string _discriminatorKey = "$type"; public PolymorphicTypeConverter(JsonSerializerOptions innerOptions) { // 复制原有配置,但移除当前转换器避免递归 _innerOptions = new JsonSerializerOptions(innerOptions) { Converters = { new PolymorphicTypeConverter<TBase>(innerOptions) } .Where(c => c.GetType() != typeof(PolymorphicTypeConverter<TBase>)) .ToList() }; } public override TBase? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // 读取原始JSON到JsonElement using var jsonDoc = JsonDocument.ParseValue(ref reader); var root = jsonDoc.RootElement; // 重排键位 var reorderedObj = new JsonObject(); if (root.TryGetProperty(_discriminatorKey, out var discrProp)) { reorderedObj.Add(_discriminatorKey, discrProp.Clone()); } foreach (var prop in root.EnumerateObject()) { if (prop.Name != _discriminatorKey) { reorderedObj.Add(prop.Name, prop.Value.Clone()); } } // 使用重新排序后的JSON进行反序列化 return JsonSerializer.Deserialize<TBase>(reorderedObj, _innerOptions); } public override void Write(Utf8JsonWriter writer, TBase value, JsonSerializerOptions options) { // 序列化时System.Text.Json会自动将鉴别器放在首位,直接使用默认逻辑 JsonSerializer.Serialize(writer, value, value.GetType(), options); } }
注意事项
- 若自定义了类型鉴别器的名称(而非默认的
$type),需同步修改中间件或转换器中的对应键名。 - 中间件中必须调用
EnableBuffering(),因为默认请求体流仅支持单次读取,开启缓冲后才能多次读取并修改。 - 序列化阶段无需额外处理,System.Text.Json原生会将类型鉴别器放在JSON对象的首位。
内容的提问来源于stack exchange,提问作者silent
相关产品推荐
相关产品推荐

