System.Text.Json: 如何反序列化具有接口属性的类(.NET 6)

huangapple go评论78阅读模式
英文:

System.Text.Json: How to deserialize class with interface properties (.NET 6)

问题

我目前正在将代码从Newtonsoft迁移到System.Text.Json。Newtonsoft能够自动反序列化具有一个或多个接口属性的对象。但是,使用System.Text.Json时,当我尝试实现相同的功能时,会收到以下错误消息:

“反序列化构造函数中的每个参数都必须绑定到反序列化的对象属性或字段。每个参数名称必须与对象上的属性或字段匹配。”

我可以通过编写自定义转换器来避免这个问题,但对于具有多个嵌套层的对象,其中每个接口属性都可以再次具有多个接口属性(因此需要多个自定义转换器),这将导致很多额外工作。是否有更简单的解决方案来解决这个问题?

我创建了一个示例来说明这个问题:

 [Fact]
public void JsonTest()
{

    var child = new Child(new Name("Peter"), 10);
    var parent = new Parent(child);

    var str = JsonSerializer.Serialize(parent);
    var jsonObj = JsonSerializer.Deserialize<Parent>(str);
    Console.WriteLine(jsonObj!.Child.Name);
}
}

[JsonConverter(typeof(ParentConverter))]
public class Parent
{
public Parent(Child child)
{
    Child = child;
}

public IChild Child { get; set;}
}

[JsonConverter(typeof(ChildConverter))]
public class Child : IChild
{
[JsonConstructor]
public Child(Name name, int age)
{
    Name = name;
    Age = age;
}

public IName Name { get; set; }
public int Age { get; set; }
}

public class Name : IName
{
public string NameValue { get; set; }
public Name(string nameValue)
{
    NameValue = nameValue;
}
}

public interface IChild
{
IName Name { get; }
int Age { get; }
}

public interface IName
{
string NameValue { get; }
}

public class ParentConverter : JsonConverter<Parent>
{
public override Parent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    using (JsonDocument document = JsonDocument.ParseValue(ref reader))
    {
        JsonElement root = document.RootElement;

        if (root.TryGetProperty("Child", out JsonElement childElement))
        {
            Child? child = JsonSerializer.Deserialize<Child>(childElement.GetRawText(), options);

            return new Parent(child!);
        }
    }

    throw new JsonException("Invalid JSON data");
}

public override void Write(Utf8JsonWriter writer, Parent value, JsonSerializerOptions options)
{
    writer.WriteStartObject();

    writer.WritePropertyName("Child");
    JsonSerializer.Serialize(writer, value.Child, options);

    writer.WriteEndObject();
}
}

public class ChildConverter : JsonConverter<Child>
{
public override Child Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    using (JsonDocument document = JsonDocument.ParseValue(ref reader))
    {
        var name = new Name("Alex");
        var age = 20;

        JsonElement root = document.RootElement;

        if (root.TryGetProperty("Name", out JsonElement nameElement))
        {
            name = JsonSerializer.Deserialize<Name>(nameElement.GetRawText(), options);
        }

        if (root.TryGetProperty("Age", out JsonElement ageElement))
        {
            age = JsonSerializer.Deserialize<int>(ageElement.GetRawText(), options);
        }
        return new Child(name!, age);
    }

    throw a JsonException("Invalid JSON data");
}

public override void Write(Utf8JsonWriter writer, Child value, JsonSerializerOptions options)
{
    writer.WriteStartObject();

    writer.WritePropertyName("Name");
    JsonSerializer.Serialize(writer, value.Name, options);

    writer.WritePropertyName("Age");
    JsonSerializer.Serialize(writer, value.Age, options);

    writer.WriteEndObject();
}
}

使用这些自定义转换器,我获得了预期的行为。但是否有一种方法可以避免为每个这样的类编写自定义转换器?

英文:

I'm currently in the process of migrating vom Newtonsoft to System.Text.Json. Newtonsoft was able to automatically deserialize objects, that have one or more Interface properties. With System.Text.Json I get the following error message for the respective classes when I try to accomplish the same:

each parameter in the deserialization constructor must bind to an object property or field on deserialization. Each parameter name must match with a property or field on the object.

I am able to avoid this problem by writing custom converters, but this would result in a lot of overhead for objects with multiple nested layers, where each Interface property can again have multiple Interface properties on their own (thus requiring multiple custom converters). Is there a simpler solution for that problem?

I create an example to illustrate the problem:

 [Fact]
public void JsonTest()
{
var child = new Child(new Name(&quot;Peter&quot;), 10);
var parent = new Parent(child);
var str = JsonSerializer.Serialize(parent);
var jsonObj = JsonSerializer.Deserialize&lt;Parent&gt;(str);
Console.WriteLine(jsonObj!.Child.Name);
}
}
[JsonConverter(typeof(ParentConverter))]
public class Parent
{
public Parent(Child child)
{
Child = child;
}
public IChild Child { get; set;}
}
[JsonConverter(typeof(ChildConverter))]
public class Child : IChild
{
[JsonConstructor]
public Child(Name name, int age)
{
Name = name;
Age = age;
}
public IName Name { get; set; }
public int Age { get; set; }
}
public class Name : IName
{
public string NameValue { get; set; }
public Name(string nameValue)
{
NameValue = nameValue;
}
}
public interface IChild
{
IName Name { get; }
int Age { get; }
}
public interface IName
{
string NameValue { get; }
}
public class ParentConverter : JsonConverter&lt;Parent&gt;
{
public override Parent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using (JsonDocument document = JsonDocument.ParseValue(ref reader))
{
JsonElement root = document.RootElement;
if (root.TryGetProperty(&quot;Child&quot;, out JsonElement childElement))
{
Child? child = JsonSerializer.Deserialize&lt;Child&gt;(childElement.GetRawText(), options);
return new Parent(child!);
}
}
throw new JsonException(&quot;Invalid JSON data&quot;);
}
public override void Write(Utf8JsonWriter writer, Parent value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WritePropertyName(&quot;Child&quot;);
JsonSerializer.Serialize(writer, value.Child, options);
writer.WriteEndObject();
}
}
public class ChildConverter : JsonConverter&lt;Child&gt;
{
public override Child Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using (JsonDocument document = JsonDocument.ParseValue(ref reader))
{
var name = new Name(&quot;Alex&quot;);
var age = 20;
JsonElement root = document.RootElement;
if (root.TryGetProperty(&quot;Name&quot;, out JsonElement nameElement))
{
name = JsonSerializer.Deserialize&lt;Name&gt;(nameElement.GetRawText(), options);
}
if (root.TryGetProperty(&quot;Age&quot;, out JsonElement ageElement))
{
age = JsonSerializer.Deserialize&lt;int&gt;(ageElement.GetRawText(), options);
}
return new Child(name!, age);
}
throw new JsonException(&quot;Invalid JSON data&quot;);
}
public override void Write(Utf8JsonWriter writer, Child value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WritePropertyName(&quot;Name&quot;);
JsonSerializer.Serialize(writer, value.Name, options);
writer.WritePropertyName(&quot;Age&quot;);
JsonSerializer.Serialize(writer, value.Age, options);
writer.WriteEndObject();
}
}

With these custom converters I get the intended behaviour. But is there a way to avoid writing a custom converter for each such classes?

答案1

得分: 3

这被称为多态序列化。库需要一种方法来注释属性实际上是IChild的哪个具体实现。我认为 Newtonsoft 添加了一个带有完整类型名称的属性,虽然这很容易使用,但如果您想要重命名类,它可能会有一些潜在的缺点。

System.Text.Json 使用属性来注释派生类型,并要求您提供标识符。了解更多关于多态类型识别器的信息。

[JsonDerivedType(typeof(Child1), typeDiscriminator: "Child1")]
[JsonDerivedType(typeof(Child2), typeDiscriminator: "Child2")]
public interface IChild
{
    int Age { get; }
}

public class Child1 : IChild
{
    public string PreferedToy { get; set; }
    public int Age { get; set; }
}
public class Child2 : IChild
{
    public string Name { get; set; }
    public int Age { get; set; }
}

如果每个接口只有一个实现,我会考虑要么摒弃接口,要么将对象转换为数据传输对象(DTO),使其尽可能简单地进行序列化。使用DTO有助于将序列化与类中的任何逻辑分离。

英文:

This is called polymorphic serialization. The library needs some way to annotate what specific implementation of IChild a property actually is. I think Newtonsoft adds a property with the complete type name, while this is easy to use, it has some potential downsides if you want to rename your class.

System.Text.Json uses attributes to annotate the derived types, and require you to supply the identifiers. Read more about Polymorphic type discriminators.

    [JsonDerivedType(typeof(Child1), typeDiscriminator: &quot;Child1&quot;)]
[JsonDerivedType(typeof(Child2), typeDiscriminator: &quot;Child2&quot;)]
public interface IChild
{
int Age { get; }
}
public class Child1 : IChild
{
public string PreferedToy { get; set; }
public int Age { get; set; }
}
public class Child2 : IChild
{
public string Name { get; set; }
public int Age { get; set; }
}

If you only have one implementation of each interface I would consider either getting rid of the interfaces, or converting your objects to Data Transfer Objects (DTOs) that are as simple to serialize as possible. Using DTOs helps separate the concerns of serializing from from any logic in your classes.

huangapple
  • 本文由 发表于 2023年6月22日 15:48:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/76529636.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定