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

如何在JSON序列化/反序列化时保留类中Object属性的类型?

如何在JSON序列化/反序列化时保留Object属性的原始类型

我对JSON序列化/反序列化经验不多,目前遇到一个棘手问题:当序列化包含Object类型属性的对象时,该属性的原始类型无法被保留,导致反序列化后无法还原为正确的类型。比如我定义了如下类:

public class InfoPoint
{
    public string Name { get; set; } = string.Empty;
    public object? Value { get; set; }
    public string Location { get; set; } = string.Empty;
}

测试代码中,InfoPoint2的Value是UInt16类型的75,但序列化后输出里Value只显示75,没有类型信息,反序列化后会变成int而不是UInt16。我已经尝试设置TypeNameHandling = TypeNameHandling.All,但值类型(比如UInt16、bool)依然没有保留类型信息,只有数组这类复杂类型才会带上$type。

复现代码:

List<InfoPoint> infoPoints = new List<InfoPoint>();

InfoPoint infoPoint1 = new InfoPoint();
infoPoint1.Name = "Point1";
bool status = true;
infoPoint1.Value = status;
infoPoint1.Location = "Main Switch Status";
infoPoints.Add(infoPoint1);

InfoPoint infoPoint2 = new InfoPoint();
infoPoint2.Name = "Point2";
UInt16 volume = 75;
infoPoint2.Value = volume;
infoPoint2.Location = "Volume Dial";
infoPoints.Add(infoPoint2);

InfoPoint infoPoint3 = new InfoPoint();
infoPoint3.Name = "Point3";
float[] sliders = new float[] { 2.3f, 1.79f, 8.77f, 5.5f };
infoPoint3.Value = sliders;
infoPoint3.Location = "Fader Panel";
infoPoints.Add(infoPoint3);

JsonSerializerSettings settings
    = new JsonSerializerSettings()
    {
        TypeNameHandling = TypeNameHandling.All,
        NullValueHandling = NullValueHandling.Ignore,
        Formatting = Formatting.Indented
    };

Console.WriteLine(JsonConvert.SerializeObject(infoPoint1, settings));
Console.WriteLine(JsonConvert.SerializeObject(infoPoint2, settings));
Console.WriteLine(JsonConvert.SerializeObject(infoPoint3, settings));
Console.WriteLine(JsonConvert.SerializeObject(infoPoints, settings));

序列化输出中,infoPoint2的Value没有类型标记:

{
  "$type": "JsonObjectListTest.InfoPoint, JsonObjectListTest",
  "Name": "Point2",
  "Value": 75,
  "Location": "Volume Dial"
}

我需要确保反序列化时Value能还原为UInt16类型,恳请提供解决方案。


解决方案

方法1:自定义JsonConverter处理值类型的类型标记

Newtonsoft.Json默认不会给值类型添加$type,因为JSON本身的基础类型(比如布尔、数字)没有区分.NET的具体值类型。我们可以写一个自定义转换器,给所有Object类型的属性值添加类型信息,反序列化时根据类型信息还原。

public class ObjectWithTypeConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(object);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.StartObject)
        {
            var jObj = JObject.Load(reader);
            var typeName = jObj["$type"]?.ToString();
            if (typeName == null)
                return jObj.ToObject<object>(serializer);
            
            var targetType = Type.GetType(typeName);
            if (targetType == null)
                throw new InvalidOperationException($"无法找到类型 {typeName}");
            
            return jObj["$value"].ToObject(targetType, serializer);
        }
        // 处理没有包装的情况(兼容原有数据)
        return serializer.Deserialize(reader, objectType)!;
    }

    public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
    {
        if (value == null)
        {
            writer.WriteNull();
            return;
        }

        var valueType = value.GetType();
        // 跳过已经被TypeNameHandling处理的复杂类型(比如数组)
        if (!valueType.IsValueType || valueType == typeof(string))
        {
            serializer.Serialize(writer, value);
            return;
        }

        // 包装值类型,添加$type和$value
        writer.WriteStartObject();
        writer.WritePropertyName("$type");
        writer.WriteValue(valueType.AssemblyQualifiedName);
        writer.WritePropertyName("$value");
        serializer.Serialize(writer, value);
        writer.WriteEndObject();
    }
}

使用时,在JsonSerializerSettings中添加这个转换器:

JsonSerializerSettings settings = new JsonSerializerSettings()
{
    TypeNameHandling = TypeNameHandling.All,
    NullValueHandling = NullValueHandling.Ignore,
    Formatting = Formatting.Indented,
    Converters = new List<JsonConverter> { new ObjectWithTypeConverter() }
};

此时序列化infoPoint2的输出会变成:

{
  "$type": "JsonObjectListTest.InfoPoint, JsonObjectListTest",
  "Name": "Point2",
  "Value": {
    "$type": "System.UInt16, System.Private.CoreLib",
    "$value": 75
  },
  "Location": "Volume Dial"
}

反序列化时就能正确还原为UInt16类型。

方法2:修改InfoPoint类,显式存储类型信息

如果不想用自定义转换器,可以给InfoPoint类添加一个额外的属性来存储Value的类型名称,序列化时保存类型,反序列化时根据类型转换。

public class InfoPoint
{
    public string Name { get; set; } = string.Empty;
    public object? Value { get; set; }
    public string? ValueType { get; set; }
    public string Location { get; set; } = string.Empty;

    // 序列化前设置ValueType
    [OnSerializing]
    internal void OnSerializingMethod(StreamingContext context)
    {
        if (Value != null)
        {
            ValueType = Value.GetType().AssemblyQualifiedName;
        }
    }

    // 反序列化后转换Value类型
    [OnDeserialized]
    internal void OnDeserializedMethod(StreamingContext context)
    {
        if (Value != null && !string.IsNullOrEmpty(ValueType))
        {
            var targetType = Type.GetType(ValueType);
            if (targetType != null)
            {
                Value = Convert.ChangeType(Value, targetType);
            }
        }
    }
}

这种方法不需要修改序列化设置,序列化后会多出ValueType字段,反序列化时自动转换类型。但缺点是增加了类的属性,JSON数据会多出一个字段。

方法3:使用Json.NET的TypeNameHandling.Objects并结合自定义设置

注意到TypeNameHandling.All会给所有类型加标记,但值类型依然不会被包装。可以尝试设置TypeNameHandling = TypeNameHandling.Objects,同时配合SerializationBinder来处理类型,但这个方法对值类型的支持依然有限,不如自定义转换器可靠。


注意事项

  • 使用自定义转换器时,要注意兼容原有数据(比如已经序列化的没有类型标记的值),转换器中需要处理这种情况。
  • 类型名称使用AssemblyQualifiedName可以确保类型能被正确找到,但要注意程序集名称的一致性(比如不同环境下的程序集名称变化)。
  • 如果是跨平台或跨应用程序序列化,要确保类型在两端都能被解析。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 15:48:13