You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 01:05:11