当$type不是第一个属性时,System.Text.Json多态数据库化异常

8wigbo56  于 5个月前  发布在  其他
关注(0)|答案(1)|浏览(66)

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

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

字符串
现在我有了两个JSON字符串:第一个JSON字符串的第一个属性是类型$type,第二个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中尝试过了。

cwtwac6a

cwtwac6a1#

从.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"

如果使用不同的类型判别器,请将适当的 predicate 传递给重载:

MoveMetadataToBeginning<TJsonNode>(this TJsonNode? node, Predicate<string> isTypeDiscriminator)

  • Json.NET也有类似的限制,但可以通过在设置中启用MetadataPropertyHandling.ReadAhead来克服。
  • 根据微软的说法,这一限制是出于性能原因。详情请参阅Eirik Tsarpalis的评论。

演示小提琴here

相关问题