System.Text.Json序列化派生类属性



我正在将.NET 5中的一个项目从Newtonsoft.Json迁移到System.Text.Json

我有课:

abstract class Car
{
public string Name { get; set; } = "Default Car Name";
}
class Tesla : Car
{
public string TeslaEngineName { get; set; } = "My Tesla Engine";
}

我试过了:

var cars = new List<Car> 
{ 
new Tesla(),
new Tesla(),
new Tesla()
};
var json = JsonSerializer.Serialize(cars);
Console.WriteLine(json);

输出为:

[
{"Name":"Default Car Name"},
{"Name":"Default Car Name"},
{"Name":"Default Car Name"}
]

它丢失了我的财产:TeslaEngineName

那么,如何序列化具有所有属性的派生对象呢?

这是System.Text.Json的一个设计功能,请参阅此处了解更多详细信息。您的选择是:

  1. 继续使用JSON.Net

  2. 使用其中一种解决方法,在这种情况下,在序列化时使用List<object>或强制转换列表。例如:

    var json = JsonSerializer.Serialize(cars.Cast<object>());
    ^^^^^^^^^^^^^^
    

    这个选项的缺点是它仍然不会序列化派生类的任何嵌套属性。

当我为个人项目生成一些JSON时,我也遇到了这个问题-我有一个递归多态数据模型,我想把它变成JSON,但根对象中存在的子对象是根对象的派生类型,这使得序列化器只会为所有派生类型吐出基类型的属性。我摆弄了JsonConverter和反思了几个小时,想出了一个大锤式的解决方案,它做到了我需要它做的事情。

基本上,这是手动遍历图中的每个对象,并使用默认序列化程序序列化不是引用类型的每个成员,但在遇到引用类型(不是字符串(的实例时,我动态生成一个新的JsonConverter来处理该类型,并将其添加到";转换器";列表,然后递归地使用该新实例将子对象序列化为其真正的运行时类型。

您可能可以将其作为解决方案的起点,该解决方案可以满足您的需求。

转换器:

/// <summary>
/// Instructs the JsonSerializer to serialize an object as its runtime type and not the type parameter passed into the Write function.
/// </summary>
public class RuntimeTypeJsonConverter<T> : JsonConverter<T>
{    
private static readonly Dictionary<Type, PropertyInfo[]> _knownProps = new Dictionary<Type, PropertyInfo[]>(); //cache mapping a Type to its array of public properties to serialize
private static readonly Dictionary<Type, JsonConverter> _knownConverters = new Dictionary<Type, JsonConverter>(); //cache mapping a Type to its respective RuntimeTypeJsonConverter instance that was created to serialize that type. 
private static readonly Dictionary<Type, Type> _knownGenerics = new Dictionary<Type, Type>(); //cache mapping a Type to the type of RuntimeTypeJsonConverter generic type definition that was created to serialize that type
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsClass && typeToConvert != typeof(string); //this converter is only meant to work on reference types that are not strings
}
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var deserialized = JsonSerializer.Deserialize(ref reader, typeToConvert, options); //default read implementation, the focus of this converter is the Write operation
return (T)deserialized;
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{            
if (value is IEnumerable) //if the value is an IEnumerable of any sorts, serialize it as a JSON array. Note that none of the properties of the IEnumerable are written, it is simply iterated over and serializes each object in the IEnumerable
{             
WriteIEnumerable(writer, value, options);
}
else if (value != null && value.GetType().IsClass == true) //if the value is a reference type and not null, serialize it as a JSON object.
{
WriteObject(writer, value, ref options);    
}       
else //otherwise just call the default serializer implementation of this Converter is asked to serialize anything not handled in the other two cases
{
JsonSerializer.Serialize(writer, value);
}
}
/// <summary>
/// Writes the values for an object into the Utf8JsonWriter
/// </summary>
/// <param name="writer">The writer to write to.</param>
/// <param name="value">The value to convert to Json.</param>
/// <param name="options">An object that specifies the serialization options to use.</param>
private void WriteObject(Utf8JsonWriter writer, T value, ref JsonSerializerOptions options)
{
var type = value.GetType();
//get all the public properties that we will be writing out into the object
PropertyInfo[] props = GetPropertyInfos(type);
writer.WriteStartObject();
foreach (var prop in props)
{
var propVal = prop.GetValue(value);
if (propVal == null) continue; //don't include null values in the final graph
writer.WritePropertyName(prop.Name);
var propType = propVal.GetType(); //get the runtime type of the value regardless of what the property info says the PropertyType should be
if (propType.IsClass && propType != typeof(string)) //if the property type is a valid type for this JsonConverter to handle, do some reflection work to get a RuntimeTypeJsonConverter appropriate for the sub-object
{
Type generic = GetGenericConverterType(propType); //get a RuntimeTypeJsonConverter<T> Type appropriate for the sub-object
JsonConverter converter = GetJsonConverter(generic); //get a RuntimeTypeJsonConverter<T> instance appropriate for the sub-object
//look in the options list to see if we don't already have one of these converters in the list of converters in use (we may already have a converter of the same type, but it may not be the same instance as our converter variable above)
var found = false;
foreach (var converterInUse in options.Converters)
{
if (converterInUse.GetType() == generic)
{
found = true;
break;
}
}
if (found == false) //not in use, make a new options object clone and add the new converter to its Converters list (which is immutable once passed into the Serialize method).
{
options = new JsonSerializerOptions(options);
options.Converters.Add(converter);
}
JsonSerializer.Serialize(writer, propVal, propType, options);
}
else //not one of our sub-objects, serialize it like normal
{
JsonSerializer.Serialize(writer, propVal);
}
}
writer.WriteEndObject();
}
/// <summary>
/// Gets or makes RuntimeTypeJsonConverter generic type to wrap the given type parameter.
/// </summary>
/// <param name="propType">The type to get a RuntimeTypeJsonConverter generic type for.</param>
/// <returns></returns>
private Type GetGenericConverterType(Type propType)
{
Type generic = null;
if (_knownGenerics.ContainsKey(propType) == false)
{
generic = typeof(RuntimeTypeJsonConverter<>).MakeGenericType(propType);
_knownGenerics.Add(propType, generic);
}
else
{
generic = _knownGenerics[propType];
}
return generic;
}
/// <summary>
/// Gets or creates the corresponding RuntimeTypeJsonConverter that matches the given generic type defintion.
/// </summary>
/// <param name="genericType">The generic type definition of a RuntimeTypeJsonConverter.</param>
/// <returns></returns>
private JsonConverter GetJsonConverter(Type genericType)
{
JsonConverter converter = null;
if (_knownConverters.ContainsKey(genericType) == false)
{
converter = (JsonConverter)Activator.CreateInstance(genericType);
_knownConverters.Add(genericType, converter);
}
else
{
converter = _knownConverters[genericType];
}
return converter;
}

/// <summary>
/// Gets all the public properties of a Type.
/// </summary>
/// <param name="t"></param>
/// <returns></returns>
private PropertyInfo[] GetPropertyInfos(Type t)
{
PropertyInfo[] props = null;
if (_knownProps.ContainsKey(t) == false)
{
props = t.GetProperties();
_knownProps.Add(t, props);
}
else
{
props = _knownProps[t];
}
return props;
}
/// <summary>
/// Writes the values for an object that implements IEnumerable into the Utf8JsonWriter
/// </summary>
/// <param name="writer">The writer to write to.</param>
/// <param name="value">The value to convert to Json.</param>
/// <param name="options">An object that specifies the serialization options to use.</param>
private void WriteIEnumerable(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (object item in value as IEnumerable)
{
if (item == null) //preserving null gaps in the IEnumerable
{
writer.WriteNullValue();
continue;
}
JsonSerializer.Serialize(writer, item, item.GetType(), options);
}
writer.WriteEndArray();
}
}

使用:

var cars = new List<Car>
{
new Tesla(),
new Tesla(),
new Tesla()
};
var options = new JsonSerializerOptions();
options.Converters.Add(new RuntimeTypeJsonConverter<object>());
var json = JsonSerializer.Serialize(cars, cars.GetType(), options);

一个(简单(解决方案,它通过滥用上面提到的对象技巧帮助我获得了一些多态序列化支持。

PolymorpicConverter

/// <example>
/// <code>
/// [PolymorpicConverter]
/// public Vehicle[] Vehicles { get; set; } = new Vehicle[]
/// {
///   new Car(),
///   new Bicycle()
/// };
/// </code>
/// </example>
/// <remarks>
/// Converter can not be used in <see cref="JsonSerializerOptions.Converters"/> because <see cref="JsonSerializer"/>
/// caches <see cref="JsonConverter"/> by <see cref="JsonConverter.CanConvert"/> which causes recursion.
/// </remarks>
internal sealed class PolymorpicConverter
: JsonConverter<System.Object>
{
// https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-apis/
// https://stackoverflow.com/questions/65664086/system-text-json-serialize-derived-class-property
// https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to?pivots=dotnet-6-0
// https://bengribaudo.com/blog/2022/02/22/6569/recursive-polymorphic-deserialization-with-system-text-json
// https://github.com/dotnet/runtime/issues/29937 - Support polymorphic serialization through new option
// https://github.com/dotnet/runtime/issues/30083 - JsonSerializer polymorphic serialization and deserialization support
// https://github.com/dotnet/runtime/issues/30969 - [System.Text.Json] serialize/deserialize any object
// https://github.com/dotnet/runtime/issues/30969#issuecomment-535779492
// https://github.com/dotnet/runtime/pull/54328   - Add polymorphic serialization to System.Text.Json
// https://github.com/dotnet/runtime/issues/63747 - Developers can use System.Text.Json to serialize type hierarchies securely
public PolymorpicConverter() { }
public override bool CanConvert(
[DisallowNull] Type type)
{
return type.IsAssignableTo(typeof(System.Object));
}
public override System.Object Read(
[DisallowNull] ref System.Text.Json.Utf8JsonReader reader,
[DisallowNull] Type typeToConvert,
[AllowNull] System.Text.Json.JsonSerializerOptions options)
{
throw new NotImplementedException($"{nameof(PolymorpicConverter)} only supports serialization.");
}
public override void Write(
[DisallowNull] System.Text.Json.Utf8JsonWriter writer,
[DisallowNull] System.Object value,
[AllowNull] System.Text.Json.JsonSerializerOptions options)
{
if (value == null)
{
JsonSerializer.Serialize(writer, default(System.Object), options);
return;
}
// String is also an Array (of char)
if (value is System.String stringValue)
{
JsonSerializer.Serialize(writer, stringValue, options);
return;
}
// Object-trick
if (value is System.Collections.IEnumerable enumerable)
{
JsonSerializer.Serialize(writer, enumerable.Cast<System.Object>().ToArray(), options);
return;
}
// Object-trick
JsonSerializer.Serialize(writer, (System.Object)value, options);
}
}

PolymorpicConverterAttribute

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class PolymorpicConverterAttribute
: JsonConverterAttribute
{
public PolymorpicConverterAttribute()
: base(typeof(PolymorpicConverter))
{ }
}

单元测试

using System.Text.Json.Serialization;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public sealed class PolymorpicConverterTests
{
[TestMethod]
public void SerializeTest()
{
var container = new Container
{
Test = "List of Vehicles"
};

var json = System.Text.Json.JsonSerializer.Serialize(container);
Assert.IsTrue(json.Contains("Trek"));
Assert.IsTrue(json.Contains("List of Vehicles"));
Assert.IsTrue(json.Contains("My Wheels-property"));
Assert.IsTrue(json.Contains("My Make-property"));
Assert.IsTrue(json.Contains("My Model-property"));
Assert.IsTrue(json.Contains("My Manufacturer-property"));
}
public class Container
{
[PolymorpicConverter] // not required; but to validate possible interferance
[JsonPropertyName("My Test-property")]
public System.String Test { get; set; }
[PolymorpicConverter]
public Vehicle[] Vehicles { get; set; } = new Vehicle[]
{
new Car() { Make = "BMW", Model = "E92" },
new Bicycle() { Manufacturer = "Trek", }
};
}
public abstract class Vehicle
{
[PolymorpicConverter] // not required; but to validate possible interferance
[JsonPropertyName("My Wheels-property")]
public System.Int32 Wheels { get; protected set; }
}
public class Car
: Vehicle
{
public Car()
{
Wheels = 4;
}
[PolymorpicConverter] // not required; but to validate possible interferance
[JsonPropertyName("My Make-property")]
public System.String Make { get; set; }
[PolymorpicConverter] // not required; but to validate possible interferance
[JsonPropertyName("My Model-property")]
public System.String Model { get; set; }
}
public class Bicycle
: Vehicle
{
public Bicycle()
{
Wheels = 2;
}
[PolymorpicConverter] // not required; but to validate possible interferance
[JsonPropertyName("My Manufacturer-property")]
public System.String Manufacturer { get; set; }
}
}

JSON-

{
"My Test-property":"List of Vehicles",
"Vehicles":[
{
"My Make-property":"BMW",
"My Model-property":"E92",
"My Wheels-property":4
},
{
"My Manufacturer-property":"Trek",
"My Wheels-property":2
}
]
}

使用此函数变体:

var json = JsonSerializer.Serialize(cars, cars.getType());

这是预期行为,并记录在案。

您可以编写自己的转换器:

public class TeslaConverter : JsonConverter<Car>
{
public override bool CanConvert(Type type)
{
return typeof(Car).IsAssignableFrom(type);
}
public override Car Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(
Utf8JsonWriter writer,
Car value,
JsonSerializerOptions options)
{
if (value is Tesla derivedA)
{
JsonSerializer.Serialize(writer, derivedA);
}
else if (value is OtherTypeDerivedFromCar derivedB)
{
JsonSerializer.Serialize(writer, derivedB);
}
else
throw new NotSupportedException();
}
}

并像这样实现:

var options = new JsonSerializerOptions
{
Converters = { new TeslaConverter () }
};
var result = JsonSerializer.Serialize(doc, options);

相关内容