嵌套场景下的多态Json反序列化

发布于 2025-01-13 02:54:15 字数 4031 浏览 4 评论 0原文

我有一些想要(反)序列化的类:

public class Top
{
    public Top(Sub content) { Content = content; }

    public Sub Content { get; init; }
}

public class Sub
{
    public Sub(Sub? entry) { Entry = entry; Type = SubType.super; }

    public Sub? Entry { get; init; }
    public SubType Type { get; init; }
}

public class SubA : Sub
{
    public SubA(Sub? entry) : base(entry) { Type = SubType.a; }
}

public enum SubType { super, a }

示例对象:

var top = new Top(new SubA(new Sub(new SubA(null))));

要序列化,我只需要使用 JsonSerializer.Serialize 和一些选项来获得我想要的:

var json = JsonSerializer.Serialize(top, new JsonSerializerOptions
    { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
      Converters = { _enumConverter } });
// result:
// {"Content":{"Entry":{"Entry":{"Type":"a"},"Type":"super"},"Type":"a"}}

反序列化不起作用盒子 - 它总是反序列化为 Sub,而不是 SubA。因此,我尝试编写自己的 JsonConverter 来查找要反序列化的类型 T(从 JSON Type 属性),然后调用适当的 JsonSerializer.Deserialize 方法。但我最终要么陷入 StackOverflow,要么在一级后丢失转换器:

public class SubConverter : JsonConverter<Sub>
{    
    public override BaseType Read(ref Utf8JsonReader reader, Type typeToConvert,
        JsonSerializerOptions options)
    {
        // Create a copy of the reader to find type. Necessary because after type was found, we
        // need to deserialize from the start, but resetting to start position is not possible.
        var typeReader = reader;

        bool discriminatorFound = false;
        while (typeReader.Read())
        {
            if (typeReader.TokenType == JsonTokenType.StartObject
                || typeReader.TokenType == JsonTokenType.StartArray)
            {
                typeReader.Skip();
                continue;
            }
            if (typeReader.TokenType != JsonTokenType.PropertyName)
                continue;
            if (typeReader.GetString() != TypeDiscriminatorPropertyName)
                continue;
            discriminatorFound = true;
            break;
        }

        if (!discriminatorFound)
            throw new JsonException(
                $"type discriminator property \"{TypeDiscriminatorPropertyName}\" was not found");

        if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.String)
            throw new JsonException("type discriminator value does not exist or is not a string");

        var typeString = typeReader.GetString();
        var deserializationType = typeString == SubType.super.ToString() ? typeof(Sub) : typeof(SubA);

        // !!!
        // if THIS, is not removed, will get infinite loop (-> StackOverflowException)
        // if THIS is removed, will not get polymorphic deserialization in properties below
        var options2 = new JsonSerializerOptions(options);
        if (options2.Converters.Contains(this))
            options2.Converters.Remove(this);
        BaseType inst = (BaseType)JsonSerializer.Deserialize(ref reader, deserializationType, options2)!;

        return inst;
    }

    // not needed; we only use this converter for deserialization
    public override void Write(Utf8JsonWriter writer, BaseType value, JsonSerializerOptions options)
    { throw new NotImplementedException(); }
}

如果我只是将 options 不变地传递到 JsonSerializer.Deserialize,我将得到一个无限循环( JsonSerializer.Deserialize 将调用 SubConverter.Read,反之亦然)。

如果我从选项中删除 SubConverter(如上面的代码所示),则以下级别中的所有内容都会丢失它。因此,而不是像这样的原始对象 foo

Top -> SubA -> Sub -> SubA

我现在得到

Top -> SubA -> Sub -> Sub
                ^      ^
                │      └─ because `SubConverter` was removed, cannot deserialize as SubA
                └─ here we removed `SubConverter`

我现在做什么?

我不想自己编写整个反序列化,只想编写必要的位。 (我的真实用例比这个问题中的类复杂得多。)

I have some classes that I want to (de-)serialize:

public class Top
{
    public Top(Sub content) { Content = content; }

    public Sub Content { get; init; }
}

public class Sub
{
    public Sub(Sub? entry) { Entry = entry; Type = SubType.super; }

    public Sub? Entry { get; init; }
    public SubType Type { get; init; }
}

public class SubA : Sub
{
    public SubA(Sub? entry) : base(entry) { Type = SubType.a; }
}

public enum SubType { super, a }

Example object:

var top = new Top(new SubA(new Sub(new SubA(null))));

To serialize, I just need to use JsonSerializer.Serialize with some options to get what I want:

var json = JsonSerializer.Serialize(top, new JsonSerializerOptions
    { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
      Converters = { _enumConverter } });
// result:
// {"Content":{"Entry":{"Entry":{"Type":"a"},"Type":"super"},"Type":"a"}}

Deserializing does not work out of the box - it always deserializes to Sub, never to SubA. So I tried writing my own JsonConverter that finds the type T to deserialize to (from the JSON Type property), then calls the appropriate JsonSerializer.Deserialize<T> method. But I end up either in a StackOverflow or in losing my converter after one level:

public class SubConverter : JsonConverter<Sub>
{    
    public override BaseType Read(ref Utf8JsonReader reader, Type typeToConvert,
        JsonSerializerOptions options)
    {
        // Create a copy of the reader to find type. Necessary because after type was found, we
        // need to deserialize from the start, but resetting to start position is not possible.
        var typeReader = reader;

        bool discriminatorFound = false;
        while (typeReader.Read())
        {
            if (typeReader.TokenType == JsonTokenType.StartObject
                || typeReader.TokenType == JsonTokenType.StartArray)
            {
                typeReader.Skip();
                continue;
            }
            if (typeReader.TokenType != JsonTokenType.PropertyName)
                continue;
            if (typeReader.GetString() != TypeDiscriminatorPropertyName)
                continue;
            discriminatorFound = true;
            break;
        }

        if (!discriminatorFound)
            throw new JsonException(
                
quot;type discriminator property \"{TypeDiscriminatorPropertyName}\" was not found");

        if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.String)
            throw new JsonException("type discriminator value does not exist or is not a string");

        var typeString = typeReader.GetString();
        var deserializationType = typeString == SubType.super.ToString() ? typeof(Sub) : typeof(SubA);

        // !!!
        // if THIS, is not removed, will get infinite loop (-> StackOverflowException)
        // if THIS is removed, will not get polymorphic deserialization in properties below
        var options2 = new JsonSerializerOptions(options);
        if (options2.Converters.Contains(this))
            options2.Converters.Remove(this);
        BaseType inst = (BaseType)JsonSerializer.Deserialize(ref reader, deserializationType, options2)!;

        return inst;
    }

    // not needed; we only use this converter for deserialization
    public override void Write(Utf8JsonWriter writer, BaseType value, JsonSerializerOptions options)
    { throw new NotImplementedException(); }
}

If I just pass the options unchanged into JsonSerializer.Deserialize, I will get an inifinite loop (JsonSerializer.Deserialize will call SubConverter.Read and vice versa).

If I remove the SubConverter from the options as in the code above, I lose it for all content in the levels below. So instead of the original object foo that was like this:

Top -> SubA -> Sub -> SubA

I now get

Top -> SubA -> Sub -> Sub
                ^      ^
                │      └─ because `SubConverter` was removed, cannot deserialize as SubA
                └─ here we removed `SubConverter`

What do I do now?

I do not want to write the whole deserialization on my own, only the necessary bit(s). (My real use case is much more complex than the classes in this question.)

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

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

发布评论

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

评论(1

情何以堪。 2025-01-20 02:54:15

我在 如何编写自定义转换器从 Newtonsoft.Json 迁移到 System.Text.Json:解决方案是注册转换器,而不是通过JsonSerializerOptions 但通过 POCO 属性上的 JsonConverterAttribute (不是 on 类型,这也会导致无限递归!)。

首先,我们从我的问题中获取 SubConverter 并将其 Write 方法更改为使用默认序列化(因为当 Converter code> 通过 JsonConverterAttribute 注册,它将用于序列化和反序列化):

public override void Write(Utf8JsonWriter writer, BaseType value, JsonSerializerOptions options)
{
    JsonSerializer.Serialize(writer, value, options);
}

第二,我们从内部删除 options2 Read 方法,剩下

...
BaseType inst = (BaseType)JsonSerializer.Deserialize(ref reader, deserializationType, options)!;
...

第三,我们将属性 [JsonConverter(typeof(SubConverter))] 添加到 Content< Top 的 /code> 属性和 SubEntry 属性。

现在我们可以像这样简单地进行(反)序列化:

var options = new JsonSerializerOptions
    { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
      Converters = { _enumConverter } };
var json = JsonSerializer.Serialize(top, options);
var fromJson = JsonDeserializer<Top>(json, options);

I found help in the docs on how to write a custom converter and migrate from Newtonsoft.Json to System.Text.Json: The solution is to register the converter not via JsonSerializerOptions but via JsonConverterAttribute on the properties of my POCOs (not the on type, this will lead to inifinite recursion as well!).

First, we take the SubConverter from my question and change its Write method to use the default serialization (because when a Converter is registered via JsonConverterAttribute, it will be used for both serialization and deserialization):

public override void Write(Utf8JsonWriter writer, BaseType value, JsonSerializerOptions options)
{
    JsonSerializer.Serialize(writer, value, options);
}

Second, we remove the options2 from inside the Read method, leaving us with

...
BaseType inst = (BaseType)JsonSerializer.Deserialize(ref reader, deserializationType, options)!;
...

Third, we add the attribute [JsonConverter(typeof(SubConverter))] to both the Content property of Top and the Entry property of Sub.

Now we can simply do (de-)serialization like this:

var options = new JsonSerializerOptions
    { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
      Converters = { _enumConverter } };
var json = JsonSerializer.Serialize(top, options);
var fromJson = JsonDeserializer<Top>(json, options);
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文