当$type不是第一个属性时,System.Text.Json多态反序列化异常

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

我注意到一些令我惊讶的行为。我有一个抽象基类和一个派生类。

[JsonPolymorphic]
[JsonDerivedType(typeof(DerivedClass), "derived")]
public abstract class BaseClass
{
    public BaseClass() { }
}
public class DerivedClass : BaseClass
{
    public string? Whatever { get; set; }
}

现在我有两个 JSON 字符串:第一个 JSON 具有类型鉴别器 (

$type
) 作为 JSON 中的第一个属性 - 第二个 JSON 字符串没有。当我执行
JsonSerializer.Deserialize<BaseClass>()
时,第二个 JSON 字符串会引发异常。

var jsonWorks = "{\"$type\": \"derived\", \"whatever\": \"Bar\"}";
var jsonBreaks = "{\"whatever\": \"Bar\", \"$type\": \"derived\"}";

var obj1 = JsonSerializer.Deserialize<BaseClass>(jsonWorks);
var obj2 = JsonSerializer.Deserialize<BaseClass>(jsonBreaks); // This one will throw an exception

抛出的异常属于

System.NotSupportedException
类型,并带有以下消息:

System.NotSupportedException: 'Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'MyApp.BaseClass'. Path: $ | LineNumber: 0 | BytePositionInLine: 12.'

它还有一个内部异常:

NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'MyApp.BaseClass'.

这种行为是预期的还是实际上是

System.Text.Json
中的潜在错误?这已在
net8.0
中尝试过。

c# json .net json-deserialization system.text.json
1个回答
0
投票

从 .NET 8 开始,这是 System.Text.Json 的记录限制。来自多态类型鉴别器

注意

类型鉴别器必须放置在 JSON 对象的开头,与其他元数据属性(如

$id
$ref
)分组在一起。

如果由于某种原因您无法在常规属性之前序列化元数据属性,则需要在反序列化之前手动修复 JSON,例如通过预加载到

JsonNode
层次结构并递归地修复属性顺序。

为此,首先定义以下扩展方法:

public static partial class JsonExtensions
{
    const string Id = "$id";
    const string Ref = "$ref";
    
    static bool DefaultIsTypeDiscriminator(string s) => s == "$type";
    
    public static TJsonNode? MoveMetadataToBeginning<TJsonNode>(this TJsonNode? node) where TJsonNode : JsonNode => node.MoveMetadataToBeginning(DefaultIsTypeDiscriminator);

    public static TJsonNode? MoveMetadataToBeginning<TJsonNode>(this TJsonNode? node, Predicate<string> isTypeDiscriminator) where TJsonNode : JsonNode
    {
        ArgumentNullException.ThrowIfNull(isTypeDiscriminator);
        foreach (var n in node.DescendantsAndSelf().OfType<JsonObject>())
        {
            var properties = n.ToLookup(p => isTypeDiscriminator(p.Key) || p.Key == Id || p.Key == Ref);
            var newProperties = properties[true].Concat(properties[false]).ToList();
            n.Clear();
            newProperties.ForEach(p => n.Add(p));
        }
        return node;
    } 
    
    // From this answer https://stackoverflow.com/a/73887518/3744182
    // To https://stackoverflow.com/questions/73887517/how-to-recursively-descend-a-system-text-json-jsonnode-hierarchy-equivalent-to
    public static IEnumerable<JsonNode?> Descendants(this JsonNode? root) => root.DescendantsAndSelf(false);

    /// Recursively enumerates all JsonNodes in the given JsonNode object in document order.
    public static IEnumerable<JsonNode?> DescendantsAndSelf(this JsonNode? root, bool includeSelf = true) => 
        root.DescendantItemsAndSelf(includeSelf).Select(i => i.node);
    
    /// Recursively enumerates all JsonNodes (including their index or name and parent) in the given JsonNode object in document order.
    public static IEnumerable<(JsonNode? node, int? index, string? name, JsonNode? parent)> DescendantItemsAndSelf(this JsonNode? root, bool includeSelf = true) => 
        RecursiveEnumerableExtensions.Traverse(
            (node: root, index: (int?)null, name: (string?)null, parent: (JsonNode?)null),
            (i) => i.node switch
            {
                JsonObject o => o.AsDictionary().Select(p => (p.Value, (int?)null, p.Key.AsNullableReference(), i.node.AsNullableReference())),
                JsonArray a => a.Select((item, index) => (item, index.AsNullableValue(), (string?)null, i.node.AsNullableReference())),
                _ => i.ToEmptyEnumerable(),
            }, includeSelf);
    
    static IEnumerable<T> ToEmptyEnumerable<T>(this T item) => Enumerable.Empty<T>();
    static T? AsNullableReference<T>(this T item) where T : class => item;
    static Nullable<T> AsNullableValue<T>(this T item) where T : struct => item;
    static IDictionary<string, JsonNode?> AsDictionary(this JsonObject o) => o;
}

public static partial class RecursiveEnumerableExtensions
{
    // Rewritten from the answer by Eric Lippert https://stackoverflow.com/users/88656/eric-lippert
    // to "Efficient graph traversal with LINQ - eliminating recursion" http://stackoverflow.com/questions/10253161/efficient-graph-traversal-with-linq-eliminating-recursion
    // to ensure items are returned in the order they are encountered.
    public static IEnumerable<T> Traverse<T>(
        T root,
        Func<T, IEnumerable<T>> children, bool includeSelf = true)
    {
        if (includeSelf)
            yield return root;
        var stack = new Stack<IEnumerator<T>>();
        try
        {
            stack.Push(children(root).GetEnumerator());
            while (stack.Count != 0)
            {
                var enumerator = stack.Peek();
                if (!enumerator.MoveNext())
                {
                    stack.Pop();
                    enumerator.Dispose();
                }
                else
                {
                    yield return enumerator.Current;
                    stack.Push(children(enumerator.Current).GetEnumerator());
                }
            }
        }
        finally
        {
            foreach (var enumerator in stack)
                enumerator.Dispose();
        }
    }
}

然后你可以做:

var obj1 = JsonNode.Parse(jsonWorks).MoveMetadataToBeginning().Deserialize<BaseClass>();
var obj2 = JsonNode.Parse(jsonBreaks).MoveMetadataToBeginning().Deserialize<BaseClass>();

备注:

  • System.Text.Json 没有硬编码的类型鉴别器名称,因此上面的代码假设类型鉴别器具有默认名称

    "$type"

    如果您使用不同的类型鉴别器,请向重载传递适当的谓词:

     MoveMetadataToBeginning<TJsonNode>(this TJsonNode? node, Predicate<string> isTypeDiscriminator)
    
  • Json.NET 也有类似的限制,但是可以通过在设置中启用

    MetadataPropertyHandling.ReadAhead
    来克服。

  • 根据 MSFT 的说法,此限制是出于性能原因而做出的。有关详细信息,请参阅 Eirik Tsarpalis 的此评论

演示小提琴在这里

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