System.Text.Json 序列化不适用于抽象成员

问题描述 投票:0回答:1

我有以下接口及其实现(带有

Newtonsoft.Json
System.Text.Json
的 JSON 序列化器):

public interface IAmount {
    decimal Value { get; }
}

[Newtonsoft.Json.JsonConverter(typeof(NewtonsoftJsonConverter))]
[System.Text.Json.Serialization.JsonConverter(typeof(SystemTextJsonConverter))]
public class Amount : IAmount {
    public Amount(decimal value) {
        Value = value;
    }

    public decimal Value { get; }
}

public class NewtonsoftJsonConverter : Newtonsoft.Json.JsonConverter {
    public override bool CanConvert(Type objectType) => objectType.IsAssignableTo(typeof(IAmount));

    public override object? ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object? existingValue, Newtonsoft.Json.JsonSerializer serializer) {
        throw new NotImplementedException();
    }

    public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object? value, Newtonsoft.Json.JsonSerializer serializer) {
        writer.WriteRawValue(((IAmount?)value)?.Value.ToString());
    }
}

public class SystemTextJsonConverter : System.Text.Json.Serialization.JsonConverter<object> {
    public override bool CanConvert(Type typeToConvert) => typeToConvert.IsAssignableTo(typeof(IAmount));

    public override object Read(ref System.Text.Json.Utf8JsonReader reader, Type typeToConvert, System.Text.Json.JsonSerializerOptions options) {
        throw new NotImplementedException();
    }

    public override void Write(System.Text.Json.Utf8JsonWriter writer, object value, System.Text.Json.JsonSerializerOptions options) {
        writer.WriteRawValue(((IAmount)value).Value.ToString());
    }
}

如果我的对象属于

Amount
类型,则效果很好。例如(在每行旁边的注释中输出):

var foo = new Amount(10);

Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(foo)); // 10
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(foo)); // 10

但是,如果对象的类型为

IAmount
,则它适用于
Newtonsoft.Json
,但不适用于
System.Text.Json
。例如:

IAmount foo = new Amount(10);

Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(foo)); // 10
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(foo)); // {"Value":10}

正如您所看到的,使用

System.Text.Json
时输出是不同的。我尝试在
CanCovert
方法上设置断点,但它从未被调用。

我可以通过在界面上添加

[System.Text.Json.Serialization.JsonConverter(typeof(SystemTextJsonConverter))]
属性来解决此问题,但理想情况下我不想这样做。有谁知道无需修改界面即可解决此问题的替代解决方案?

请注意,切换到 Newtonsoft 不是一个选择。

c# json.net polymorphism system.text.json
1个回答
2
投票

这是设计好的。 System.Text.Json 故意在序列化期间不支持多态性,除非要序列化的对象被显式声明为

object
或(从 .NET 7 开始)启用其 选择加入对多态性的支持。来自文档

序列化派生类的属性

在 .NET 7 之前的版本中,

System.Text.Json
不支持多态类型层次结构的序列化。例如,如果属性的类型是接口或抽象类,则仅支持在接口或抽象上定义的属性即使运行时类型具有其他属性,类也会被序列化。本节解释了此行为的例外情况。有关 .NET 7 中支持的信息,请参阅.NET 7 中的多态序列化

要序列化[a]派生类型的属性,请使用以下方法之一:

  1. 调用 Serialize 的重载,让您可以在运行时指定类型...

  2. 声明要序列化的对象为

    object

虽然文档仅声明派生类的属性未序列化,但我相信,由于 System.Text.Json 内部是基于契约的序列化器,因此在序列化派生类型时,它使用声明类型的整个契约。因此,元数据(包括

JsonConverterAttribute
和已应用的任何其他 JSON 属性)以及属性是通过反映声明的类型(此处为
IAmount
)而不是实际类型(此处为
Amount
)来获取的。

那么,您有哪些选择来解决此限制?

首先,如果

IAmount
仅实现为
Amount
,您可以引入一个
JsonConverter
,它始终将一种类型序列化为其他兼容类型:

public class AbstractToConcreteConverter<TAbstract, TConcrete> : JsonConverter<TAbstract> where TConcrete : TAbstract
{
    static AbstractToConcreteConverter()
    {
        if (typeof(TAbstract) == typeof(TConcrete))
            throw new ArgumentException(string.Format("Identical type {0} used for both TAbstract and TConcrete", typeof(TConcrete)));
    }
    
    public override TAbstract? Read(ref System.Text.Json.Utf8JsonReader reader, Type typeToConvert, System.Text.Json.JsonSerializerOptions options) =>
        JsonSerializer.Deserialize<TConcrete>(ref reader, options);

    public override void Write(System.Text.Json.Utf8JsonWriter writer, TAbstract value, System.Text.Json.JsonSerializerOptions options) =>
        JsonSerializer.Serialize(writer, (TConcrete)value!, options);
}

并将其应用到

IAmount

[JsonConverter(typeof(AbstractToConcreteConverter<IAmount, Amount>))]
public interface IAmount {
    decimal Value { get; }
}

演示小提琴#1 这里

其次,如果您根本不关心反序列化,并希望将所有声明为接口的值序列化为其具体类型,您可以引入一个转换器工厂来实现这一点:

public class ConcreteInterfaceSerializer : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert) => typeToConvert.IsInterface;
    
    class ConcreteInterfaceSerializerOfType<TInterface> : JsonConverter<TInterface> 
    {
        static ConcreteInterfaceSerializerOfType()
        {
            if (!typeof(TInterface).IsAbstract && !typeof(TInterface).IsInterface)
                throw new NotImplementedException(string.Format("Concrete class {0} is not supported", typeof(TInterface)));
        }   
    
        public override TInterface? Read(ref System.Text.Json.Utf8JsonReader reader, Type typeToConvert, System.Text.Json.JsonSerializerOptions options) =>
            throw new NotImplementedException();

        public override void Write(System.Text.Json.Utf8JsonWriter writer, TInterface value, System.Text.Json.JsonSerializerOptions options) =>
            JsonSerializer.Serialize<object>(writer, value!, options);
    }
    
    public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) => 
        (JsonConverter)Activator.CreateInstance(
            typeof(ConcreteInterfaceSerializerOfType<>).MakeGenericType(new Type[] { type }),
            BindingFlags.Instance | BindingFlags.Public,
            binder: null,
            args: Array.Empty<object>(),
            culture: null).ThrowOnNull();
}

public static class ObjectExtensions
{
    public static T ThrowOnNull<T>(this T? value) where T : class => value ?? throw new ArgumentNullException();
}

或者直接将其应用于

IAmount

[JsonConverter(typeof(ConcreteInterfaceSerializer))]
public interface IAmount {
    decimal Value { get; }
}

或者在选项中添加:

var options = new JsonSerializerOptions
{
    Converters = { new ConcreteInterfaceSerializer() },
};
var systemJson = System.Text.Json.JsonSerializer.Serialize<IAmount>(foo, options);

演示小提琴 #2 这里

© www.soinside.com 2019 - 2024. All rights reserved.