我注意到一些令我惊讶的行为。我有一个抽象基类和一个派生类。
[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
中尝试过。
从 .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)
MetadataPropertyHandling.ReadAhead
来克服。
根据 MSFT 的说法,此限制是出于性能原因而做出的。有关详细信息,请参阅 Eirik Tsarpalis 的此评论。
演示小提琴在这里。