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

使用Json.Net的TypeNameHandling.Auto时,列表与单个对象序列化类型信息差异问题

为什么Json.Net的TypeNameHandling.Auto在序列化列表时显示类型信息,单个对象却不显示?

嘿,这个问题我之前踩过坑,其实是TypeNameHandling.Auto的工作逻辑在搞鬼!让我给你拆解清楚来龙去脉。

核心原因:TypeNameHandling.Auto的触发规则

TypeNameHandling.Auto的核心逻辑很简单:只有当被序列化对象的实际类型和它的「声明类型」不一致时,才会自动添加$type类型信息。这里的「声明类型」会根据序列化上下文变化:

  • 单个根对象:声明类型就是你传给JsonConvert.SerializeObject的参数类型。如果参数类型和实例类型一致(比如直接传子类实例,或者传基类实例),就不会加类型信息;如果参数是基类类型,但实际是子类实例,才会触发$type添加。
  • 集合/列表中的元素:声明类型是集合的泛型参数类型(比如List<BaseClass>的元素声明类型是BaseClass)。只要集合里的元素实际是子类,就会触发TypeNameHandling.Auto——因为元素的声明类型和实际类型不一致,所以会自动加$type。

你遇到的场景,大概率是序列化单个对象时直接传了子类实例(声明类型=实际类型),而序列化列表时用的是基类泛型列表,里面放了子类元素,所以出现了差异。

复现你的场景的完整代码

我们用代码来直观验证这个逻辑:

using System;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace SerializationTest
{
    public class BaseClass { public int Id { get; set; } }
    public class DerivedClass : BaseClass { public string Name { get; set; } }

    class Program
    {
        static void Main(string[] args)
        {
            // 情况1:直接序列化子类实例(声明类型=实际类型)
            var directSingle = new DerivedClass { Id = 1, Name = "Direct" };
            // 情况2:子类实例赋值给基类变量后序列化(声明类型≠实际类型)
            BaseClass typedSingle = new DerivedClass { Id = 2, Name = "Typed" };
            // 情况3:BaseClass类型列表,元素是子类实例
            List<BaseClass> itemList = new List<BaseClass> 
            { 
                new DerivedClass { Id = 3, Name = "List Item" } 
            };

            var settings = new JsonSerializerSettings
            {
                TypeNameHandling = TypeNameHandling.Auto,
                Formatting = Formatting.Indented
            };

            Console.WriteLine("1. 直接序列化子类实例:");
            Console.WriteLine(JsonConvert.SerializeObject(directSingle, settings));

            Console.WriteLine("\n2. 序列化基类变量(实际是子类):");
            Console.WriteLine(JsonConvert.SerializeObject(typedSingle, settings));

            Console.WriteLine("\n3. 序列化BaseClass列表(含子类元素):");
            Console.WriteLine(JsonConvert.SerializeObject(itemList, settings));
        }
    }
}

输出结果(完全匹配你的现象):

  1. 直接序列化子类实例:
    {
    "Id": 1,
    "Name": "Direct"
    }

  2. 序列化基类变量(实际是子类):
    {
    "$type": "SerializationTest.DerivedClass, SerializationTest",
    "Id": 2,
    "Name": "Typed"
    }

  3. 序列化BaseClass列表(含子类元素):
    [
    {
    "$type": "SerializationTest.DerivedClass, SerializationTest",
    "Id": 3,
    "Name": "List Item"
    }
    ]

解决方案

如果你的需求是让单个子类对象也显示类型信息,可以按以下两种方式处理:

  1. 规范序列化前的类型声明:把子类实例赋值给基类类型的变量后再序列化,像上面的情况2那样,让声明类型和实际类型不一致,触发TypeNameHandling.Auto添加类型信息。
  2. 强制添加类型信息(更严格的控制):如果不管声明类型是什么,都要给所有对象加类型信息,可以把TypeNameHandling设置为TypeNameHandling.Objects或TypeNameHandling.All。但要注意:这样会增加JSON体积,而且反序列化时一定要加自定义绑定器,避免类型注入攻击。

示例代码(带安全绑定器):

var settings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.Objects, // 对所有对象添加类型信息
    Formatting = Formatting.Indented,
    // 安全绑定器:只允许我们自己的基类和子类
    SerializationBinder = new CustomSerializationBinder()
};

// 自定义序列化绑定器实现
public class CustomSerializationBinder : ISerializationBinder
{
    public Type BindToType(string assemblyName, string typeName)
    {
        var allowedTypes = new[] { typeof(BaseClass), typeof(DerivedClass) };
        return allowedTypes.FirstOrDefault(t => t.FullName == typeName);
    }

    public void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        assemblyName = serializedType.Assembly.GetName().Name;
        typeName = serializedType.FullName;
    }
}

内容的提问来源于stack exchange,提问作者codigube

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 08:30:02