嵌套场景下的多态Json反序列化
我有一些想要(反)序列化的类:
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 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
我在 如何编写自定义转换器 和 从 Newtonsoft.Json 迁移到 System.Text.Json:解决方案是注册转换器,而不是通过
JsonSerializerOptions
但通过 POCO 属性上的JsonConverterAttribute
(不是 on 类型,这也会导致无限递归!)。首先,我们从我的问题中获取
SubConverter
并将其Write
方法更改为使用默认序列化(因为当Converter
code> 通过JsonConverterAttribute
注册,它将用于序列化和反序列化):第二,我们从内部删除
options2
Read
方法,剩下第三,我们将属性
[JsonConverter(typeof(SubConverter))]
添加到Content<
Top
的 /code> 属性和Sub
的Entry
属性。现在我们可以像这样简单地进行(反)序列化:
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 viaJsonConverterAttribute
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 itsWrite
method to use the default serialization (because when aConverter
is registered viaJsonConverterAttribute
, it will be used for both serialization and deserialization):Second, we remove the
options2
from inside theRead
method, leaving us withThird, we add the attribute
[JsonConverter(typeof(SubConverter))]
to both theContent
property ofTop
and theEntry
property ofSub
.Now we can simply do (de-)serialization like this: