【问题标题】:System.Text.Json serialize derived class propertySystem.Text.Json 序列化派生类属性
【发布时间】:2021-01-11 09:18:20
【问题描述】:

我正在 .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 仅使用设计指定的类型进行序列化。你需要在(反)序列化时使用Tesla类型来获取额外的属性,否则你只会得到Car属性。
  • 您可以选择使用List&lt;Tesla&gt;
  • 您说您正在将其转换为 .NET 5,这之前是否有效?如果是这样,您使用了哪个 json 库?您针对的是哪个平台/运行时?我问是因为我怀疑这是否有效。
  • 对不起,序列化。你说得对,我想到了反序列化。别介意我之前的评论。
  • 解决此类问题的suggested method 是在用于序列化的类型中使用List&lt;object&gt;。或者......留在Json.net

标签: c# json .net system.text.json


【解决方案1】:

这是 System.Text.Json 的一个设计特性,详情请参阅here。您的选择是:

  1. 继续使用 JSON.Net

  2. 使用其中一种解决方法,在这种情况下使用List&lt;object&gt; 或在序列化时转换您的列表。例如:

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

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

【讨论】:

  • 虽然有效。但不是那么理想。目前似乎没有理想的解决方案。可能需要 .NET 团队来解决。
  • 但是,如果序列化隐式发生,则不能使用此解决方案,例如从 ASP.NET 控制器返回 ActionResult&lt;...&gt;。 ?
  • 需要注意的是,这不会改变属性的序列化方式;此技巧仅适用于根对象。我找到了一种方法来说服 System.Text.Json 在序列化期间尊重属性的运行时类型:它的类型必须是object。然而,这使得属性的反序列化变得非常困难(笨重的自定义 JsonConverter 类似乎是唯一的选择,更糟糕的是,JsonConverter 需要包含自定义序列化程序)。见github.com/dotnet/runtime/issues/30083
  • @Qwertie 这个问题与 asp.net 无关,我也确实提到了你的第二点
【解决方案2】:

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

【讨论】:

    【解决方案3】:

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

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

    【讨论】:

      【解决方案4】:

      使用这个函数的变体:

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

      这是预期行为,是documented.

      【讨论】:

      • 问题在于嵌套属性没有得到同样的处理,因此您的选择归结为自定义转换器或将所有内容都设为对象类型,这也很糟糕。
      猜你喜欢
      • 2020-04-23
      • 1970-01-01
      • 1970-01-01
      • 2014-03-31
      • 1970-01-01
      • 1970-01-01
      • 2019-05-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多