如何在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

