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

10

我正在将一个.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

那么如何序列化一个带有所有属性的派生对象呢?


2
System.Text.Json 只按照设计规定的类型进行序列化。如果要获取额外的属性,则需要在 (反)序列化时使用 Tesla 类型,否则只会获取 Car 属性。 - Martin Costello
你可以使用 List<Tesla> 作为一个选项。 - Pavel Anikhouski
你说你要将它转换为.NET 5,以前做过吗?如果是这样,你使用了哪个json库?你针对哪个平台/运行时?我问这个问题是因为我怀疑这从来没有成功过。 - Lasse V. Karlsen
抱歉,我想错了,应该是反序列化。请忽略我之前的评论。 - Lasse V. Karlsen
2
修复这种问题的建议方法是在用于序列化的类型中使用List<object>。或者...继续使用Json.net。 - Lasse V. Karlsen
5个回答

4

这是 System.Text.Json 的一个设计特性,详情请参见此处。你有以下选项:

  1. Keep using JSON.Net

  2. Use one of the workarounds, in this case use a List<object> or cast your list when serialising. For example:

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

    The downside of this option is that it will still not serialise any nested properties of derived classes.


1
虽然它能工作,但并不是很理想。目前似乎没有理想的解决方案。可能需要.NET团队来解决它。 - Anduin Xue
但是,如果序列化是隐式发生的,例如从ASP.NET控制器返回ActionResult<...>,则无法使用此解决方案。 - Qwertie
2
需要注意的是,这不会改变属性序列化的方式;这个技巧只适用于根对象。我发现了一种方法可以说服System.Text.Json在序列化期间尊重属性的运行时类型:它的类型必须是“object”。然而,这使得属性的反序列化非常困难(笨拙的自定义JsonConverter类似乎是唯一的选择,更糟糕的是,JsonConverter需要包含一个自定义序列化器)。请参见https://github.com/dotnet/runtime/issues/30083 - Qwertie
@Qwertie 这个问题不是关于asp.net的,而且我也提到了你的第二点。 - DavidG

3
我在为个人项目生成一些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);

2

通过滥用上述对象技巧,我找到了一种简单的解决方案,帮助我实现了一些多态序列化支持。

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://dev59.com/mL7pa4cB1Zd3GeqP9_v_
    // 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);
    }
}

多态转换器属性

[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
      }
   ]
}

1

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

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);

1
使用此函数的变体:
var json = JsonSerializer.Serialize(cars, cars.getType());

这是预期行为,已被记录在文档中。

2
问题在于嵌套属性没有得到同样的处理,因此你的选择只能是使用自定义转换器或者让所有东西都成为对象类型,这也是不好的。 - Austin Salgat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接