system.text.json序列化对抽象成员无效

发布于 2025-02-11 02:58:16 字数 2670 浏览 0 评论 0 原文

我有以下接口及其实现(使用JSON Serializers for Newtonsoft.json System.text.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());
    }
}

如果我的对象是类型金额。例如(在每行旁边的注释中输出中的输出):

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不是一个选择。

I have the following interface and it's implementation (with JSON serializers for both Newtonsoft.Json and System.Text.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());
    }
}

This works fine if my object is of type Amount. For example (output in the comment next to each line):

var foo = new Amount(10);

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

However if the object is of type IAmount it works fine for Newtonsoft.Json but not for System.Text.Json. For example:

IAmount foo = new Amount(10);

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

As you can see the output is different when using System.Text.Json. I tried putting a breakpoint against the CanCovert method, however it was never called.

I can fix this by adding a [System.Text.Json.Serialization.JsonConverter(typeof(SystemTextJsonConverter))] attribute against the interface but ideally I don't wish to do this. Does anyone know of an alternative solution for solving this without having to modify the interface?

Note that switching to Newtonsoft isn't an option.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

∞觅青森が 2025-02-18 02:58:16

这是设计的。 system.text.json有意不支持序列化过程中的多态性,除非明确声明要序列化的对象为 object 或(从.net 7 开始) opt-opt-opt-opt-in启用。来自 docs

衍生类的序列化属性

在.net 7之前的版本中属性的类型是接口或抽象类,即使运行时类型具有其他属性,仅接口或抽象类中定义的属性是序列化的。本节中解释了这种行为的例外。有关.NET 7中支持的信息,请参见 .NET 7 中的多态性序列化。

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

  1. 调用序列化的过载,可以在运行时指定类型...

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

虽然文档仅指出属性尚未序列化,但我相信,由于system.text.json是内部基于合同的序列化器,因此它在序列化时使用声明类型的整个合同派生类型。因此,元数据(包括 jsonConverterAttribute 和任何其他 json属性已应用的)以及通过反映声明的类型(此处 iamount )而不是实际的类型(此处 noce nocal)来获取属性。 )。

那么,您可以选择解决此限制?

首先,如果 iamount 仅以量为 ,则可以引入 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; }
}

或将其添加到 iamount 无法修改,或者如果在不同情况下使用不同的混凝土类型

var options = new JsonSerializerOptions
{
    Converters = { new AbstractToConcreteConverter<IAmount, Amount>() }
    // Add other options as required
};
var systemJson = System.Text.Json.JsonSerializer.Serialize<IAmount>(foo, options);      
var foo2 = System.Text.Json.JsonSerializer.Deserialize<IAmount>(systemJson, options);

=“ nofollow noreferrer”> jsonserialializaizoptions.converters 如果 #2 此处在这里

其次,如果您根本不在乎避免化,并且希望将所有值声明为接口被序列化为混凝土类型,则可以引入一个可以做到这一点的转换器工厂:

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

演示小提琴#3 在这里

This is as designed. System.Text.Json intentionally does not support polymorphism during serialization except when the object to be serialized is explicitly declared to be object or (starting with .NET 7) its opt-in support for polymorphism is enabled. From the docs:

Serialize properties of derived classes

In versions prior to .NET 7, System.Text.Json doesn't support the serialization of polymorphic type hierarchies. For example, if a property's type is an interface or an abstract class, only the properties defined on the interface or abstract class are serialized, even if the runtime type has additional properties. The exceptions to this behavior are explained in this section. For information about support in .NET 7, see Polymorphic serialization in .NET 7.

To serialize the properties of [a] derived type, use one of the following approaches:

  1. Call an overload of Serialize that lets you specify the type at runtime...

  2. Declare the object to be serialized as object.

While the documentation only states that properties of derived classes are not serialized, I believe that, since System.Text.Json is internally a contract-based serializer, it uses the entire contract of the declared type when serializing a derived type. Thus the metadata (including JsonConverterAttribute and any other JSON attributes that have been applied) as well as the properties are taken by reflecting the declared type (here IAmount) not the actual type (here Amount).

So, what are your options to work around this restriction?

Firstly, if IAmount is only ever implemented as Amount, you could introduce a JsonConverter which always serializes one type as some other compatible type:

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

Then either apply it to IAmount:

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

Or add it in JsonSerializerOptions.Converters if IAmount cannot be modified, or if different concrete types will be used in different circumstances:

var options = new JsonSerializerOptions
{
    Converters = { new AbstractToConcreteConverter<IAmount, Amount>() }
    // Add other options as required
};
var systemJson = System.Text.Json.JsonSerializer.Serialize<IAmount>(foo, options);      
var foo2 = System.Text.Json.JsonSerializer.Deserialize<IAmount>(systemJson, options);

Demo fiddles #1 and #2 here and here.

Secondly, if you don't care about deserialization at all and want all values declared as interfaces to be serialized as their concrete types, you could introduce a converter factory that does just that:

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

And either apply it directly to IAmount:

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

Or add it in options:

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

Demo fiddle #3 here.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文