System.Text.Json.JsonElement 转为对象的解决方法

117

我想知道在System.Text.Json中,与Json.NET中的ToObject<>()方法等效的方法是什么。

使用Json.NET,您可以使用任何JToken并将其转换为类。例如:

var str = ""; // Some JSON string
var jObj = JObject.Parse(str);
var myClass = jObj["SomeProperty"].ToObject<SomeClass>();

如何使用.NET Core 3的新System.Text.Json来实现这一点?

var str = ""; // Some JSON string
var jDoc = JsonDocument.Parse(str);
var myClass = jDoc.RootElement.GetProperty("SomeProperty"). <-- now what??

起初,我想将在jDoc.RootElement.GetPRoperty("SomeProperty")中返回的JsonElement转换为字符串,然后反序列化该字符串。但我觉得这可能不是最有效的方法,并且我无法找到另一种方法的文档。

4个回答

118
在.NET 6中,JsonSerializer将添加扩展方法,可直接从JsonElementJsonDocument反序列化对象:
public static partial class JsonSerializer
{
    public static TValue? Deserialize<TValue>(this JsonDocument document, JsonSerializerOptions? options = null);
    public static object? Deserialize(this JsonDocument document, Type returnType, JsonSerializerOptions? options = null);
    public static TValue? Deserialize<TValue>(this JsonDocument document, JsonTypeInfo<TValue> jsonTypeInfo);
    public static object? Deserialize(this JsonDocument document, Type returnType, JsonSerializerContext context);

    public static TValue? Deserialize<TValue>(this JsonElement element, JsonSerializerOptions? options = null);
    public static object? Deserialize(this JsonElement element, Type returnType, JsonSerializerOptions? options = null);
    public static TValue? Deserialize<TValue>(this JsonElement element, JsonTypeInfo<TValue> jsonTypeInfo);
    public static object? Deserialize(this JsonElement element, Type returnType, JsonSerializerContext context);
}

现在你将能够执行:

using var jDoc = JsonDocument.Parse(str);
var myClass = jDoc.RootElement.GetProperty("SomeProperty").Deserialize<SomeClass>();

注意事项:

  • JsonDocument is disposable. According to the documentation, This class utilizes resources from pooled memory... failure to properly dispose this object will result in the memory not being returned to the pool, which will increase GC impact across various parts of the framework.

    So, be sure to declare your jDoc with a using statement.

  • The new methods should be present in .NET 6.0 Preview RC1.

    They were added in response to the enhancement request We should be able serialize and serialize from DOM #31274, which has been closed.

  • Similar extension methods were added for the new JsonNode mutable JSON document node as well

    public static TValue? Deserialize<TValue>(this JsonNode? node, JsonSerializerOptions? options = null)
    public static object? Deserialize(this JsonNode? node, Type returnType, JsonSerializerOptions? options = null)
    public static TValue? Deserialize<TValue>(this JsonNode? node, JsonTypeInfo<TValue> jsonTypeInfo)
    public static object? Deserialize(this JsonNode? node, Type returnType, JsonSerializerContext context)
    
在.NET 5及更早版本中,这些方法不存在。解决方法是,您可以通过编写到中间的byte缓冲区而不是字符串来获得更好的性能,因为JsonDocumentUtf8JsonReader直接使用byte跨度而不是字符串或char跨度。如文档所述:

序列化为UTF-8比使用基于字符串的方法快5-10%。差异在于字节(作为UTF-8)无需转换为字符串(UTF-16)。

public static partial class JsonExtensions
{
    public static T ToObject<T>(this JsonElement element, JsonSerializerOptions options = null)
    {
        var bufferWriter = new ArrayBufferWriter<byte>();
        using (var writer = new Utf8JsonWriter(bufferWriter))
            element.WriteTo(writer);
        return JsonSerializer.Deserialize<T>(bufferWriter.WrittenSpan, options);
    }

    public static T ToObject<T>(this JsonDocument document, JsonSerializerOptions options = null)
    {
        if (document == null)
            throw new ArgumentNullException(nameof(document));
        return document.RootElement.ToObject<T>(options);
    }
}

这里有一个演示fiddle 链接


5
18个月后,仍然在dotnet 5上使用这个答案。不过,在“element.WriteTo(writer)”之后需要加上“writer.Flush();”,否则有时会出现“System.Text.Json.JsonException:'Expected depth to be zero at the end of the JSON payload. There is an open JSON object or array that should be closed."的错误提示。 - mclayton
1
@mclayton - 我通过 using 语句处理 Utf8JsonWriter writer 的释放,并根据文档中的说明,dispose 方法 对于 IBufferWriter,在已经写入的基础上使底层IBufferWriter<T>前进;而 flush 方法在已经写入的基础上使底层IBufferWriter<T>前进。两者都进行似乎是不必要的。 - dbc
3
啊,我的错。我使用的是 C# 8.0 版本的 using 表达式形式......using var writer = new Utf8JsonWriter(bufferWriter); 这样一来,Dispose 方法只会在函数结束时调用,这完全改变了代码的行为,需要使用 Flush 来修复它。一对括号产生的差别真是大啊! - mclayton
1
@Ason - JsonSerializer 内部反序列化 utf8 字节序列,因此 JsonSerializer.Deserialize<TValue>(string json, ...) 在反序列化之前会进行 utf8 转码。我的方法直接跳过了来回转码的步骤。 - dbc
1
ArrayBufferWriter 似乎是 .NET Core 的东西。在 .NET Framework 4.8 中工作时,我能否用 Microsoft.Toolkit.HighPerformance nuget 包中的 ArrayPoolBufferWriter 替换它? - dotNET
显示剩余6条评论

105
我遇到了同样的问题,所以我写了一些扩展方法,目前运行良好。如果他们提供了内置方法来避免额外分配字符串,那就太好了。
public static T ToObject<T>(this JsonElement element)
{
    var json = element.GetRawText();
    return JsonSerializer.Deserialize<T>(json);
}
public static T ToObject<T>(this JsonDocument document)
{
    var json = document.RootElement.GetRawText();
    return JsonSerializer.Deserialize<T>(json);
}

然后按照以下方式使用:
jDoc.RootElement.GetProperty("SomeProperty").ToObject<SomeClass>();

4
我也做了同样的事情。我发布这个问题是想知道是否有更有效的方法,而不需要像你提到的那样进行字符串分配。也许我们需要等待system.text.json更加成熟一些。 - Stalfos
15
是的,虽然这并不仅仅涉及到字符串分配。更多的是关于再次解析整个JSON的文本表示。当JsonElement / Document被创建时,它已经被解析过一次了,因此这也是浪费CPU的行为。 - Elephantik
1
兄弟...这正是我想做的。非常感谢你添加了这个! - ganjeii

11
dbc的答案相同,只是包括了让您通过Type returnType指定返回类型的方法。
public static partial class JsonExtensions
{
    public static T ToObject<T>(this JsonElement element, JsonSerializerOptions options = null)
    {
        var bufferWriter = new ArrayBufferWriter<byte>();
        using (var writer = new Utf8JsonWriter(bufferWriter))
        {
            element.WriteTo(writer);
        }

        return JsonSerializer.Deserialize<T>(bufferWriter.WrittenSpan, options);
    }

    public static T ToObject<T>(this JsonDocument document, JsonSerializerOptions options = null)
    {
        if (document == null)
        {
            throw new ArgumentNullException(nameof(document));
        }

        return document.RootElement.ToObject<T>(options);
    }       

    public static object ToObject(this JsonElement element, Type returnType, JsonSerializerOptions options = null)
    {
        var bufferWriter = new ArrayBufferWriter<byte>();
        using (var writer = new Utf8JsonWriter(bufferWriter))
        {
            element.WriteTo(writer);
        }

        return JsonSerializer.Deserialize(bufferWriter.WrittenSpan, returnType, options);
    }

    public static object ToObject(this JsonDocument document, Type returnType, JsonSerializerOptions options = null)
    {
        if (document == null)
        {
            throw new ArgumentNullException(nameof(document));
        }

        return document.RootElement.ToObject(returnType, options);
    }       
}

2
将序列化回文本似乎不是一种高效的方式。 - Mightywill

10

.NET 6引入了System.Text.Json.Nodes 命名空间,几乎与Json.Net完全相同的语法提供了一种方法来实现此操作:

var str = ""; // some json string
var node = JsonNode.Parse(str);
var myClass = node["SomeProperty"].Deserialize<SomeClass>();

命名空间包括4种新类型:JsonNodeJsonArrayJsonObjectJsonValue,可用于访问或修改DOM中的值。JsonNode是其他三种类型的基类。
dbc's answer中列出的Deserialize扩展方法也已添加以操作JsonNode,例如:
public static TValue? Deserialize<TValue>(this JsonNode? node, JsonSerializerOptions? options = null);

JsonNode是不可回收的,因此您不需要使用using语法。

使用AsObject()AsArray()解析为JsonObjectJsonArray

// parse array
JsonArray arr = JsonNode.Parse(@"[{""Name"": ""Bob"", ""Age"":30}]").AsArray();
// parse object
JsonObject obj = JsonNode.Parse(@"{""Name"": ""Bob"", ""Age"":30}").AsObject();
// get a value
var date = JsonNode.Parse(@"{""Date"":""2021-12-21T13:24:46+04:00""}")["Date"].GetValue<DateTimeOffset>();

一旦解析了JSON,就可以浏览、过滤和转换DOM和/或应用Deserialize<T>()来映射到具体类型。

要将其序列化回JSON字符串,您可以使用ToJsonString(),例如:

string innerNodeJson = node["SomeProperty"].ToJsonString();

请查看 System.Text.Json 中 JObject 的等效物,了解更多关于JsonObject的详细信息。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接