使用 System.Text.Json 获取嵌套属性

Posted

技术标签:

【中文标题】使用 System.Text.Json 获取嵌套属性【英文标题】:Getting nested properties with System.Text.Json 【发布时间】:2020-08-16 14:53:34 【问题描述】:

我在我的项目中使用System.Text.Json,因为我正在处理大型文件,因此我也决定使用它来处理 GraphQL 响应。

由于 GraphQL 的性质,有时我会得到高度嵌套的响应,这些响应不固定且映射到类没有意义。我通常需要检查响应中的一些属性。

我的问题是JsonElement。检查嵌套属性感觉非常笨拙,我觉得应该有更好的方法来解决这个问题。

例如,以下面的代码模拟我得到的响应。我只想检查是否存在 2 个属性(id 和 originalSrc)以及它们是否确实获得了价值,但感觉就像我已经对代码做了一顿饭。有没有更好/更清晰/更简洁的写法?

var raw = @"
""data"": 
""products"": 
    ""edges"": [
        
            ""node"": 
                ""id"": ""gid://shopify/Product/4534543543316"",
                ""featuredImage"": 
                    ""originalSrc"": ""https://cdn.shopify.com/s/files/1/0286/pic.jpg"",
                    ""id"": ""gid://shopify/ProductImage/146345345339732""
                
            
        
    ]


";

var doc = JsonSerializer.Deserialize<JsonElement>(raw);

JsonElement node = new JsonElement();

string productIdString = null;

if (doc.TryGetProperty("data", out var data))
    if (data.TryGetProperty("products", out var products))
        if (products.TryGetProperty("edges", out var edges))
            if (edges.EnumerateArray().FirstOrDefault().ValueKind != JsonValueKind.Undefined && edges.EnumerateArray().First().TryGetProperty("node", out node))
                if (node.TryGetProperty("id", out var productId))
                    productIdString = productId.GetString();

string originalSrcString = null;

if(node.ValueKind != JsonValueKind.Undefined && node.TryGetProperty("featuredImage", out var featuredImage))
    if (featuredImage.TryGetProperty("originalSrc", out var originalSrc))
        originalSrcString = originalSrc.GetString();

if (!string.IsNullOrEmpty(productIdString))

    //do stuff


if (!string.IsNullOrEmpty(originalSrcString))

    //do stuff

这不是大量的代码,但检查少数属性是如此普遍,我想要一种更简洁、更易读的方法。

【问题讨论】:

【参考方案1】:

您可以添加几个扩展方法,通过属性名称或数组索引访问子 JsonElement 值,如果未找到则返回可为空的值:

public static partial class JsonExtensions

    public static JsonElement? Get(this JsonElement element, string name) => 
        element.ValueKind != JsonValueKind.Null && element.ValueKind != JsonValueKind.Undefined && element.TryGetProperty(name, out var value) 
            ? value : (JsonElement?)null;
    
    public static JsonElement? Get(this JsonElement element, int index)
    
        if (element.ValueKind == JsonValueKind.Null || element.ValueKind == JsonValueKind.Undefined)
            return null;
        // Throw if index < 0
        return index < element.GetArrayLength() ? element[index] : null;
    

现在可以使用空条件运算符?. 将访问嵌套值的调用链接在一起:

var doc = JsonSerializer.Deserialize<JsonElement>(raw);

var node = doc.Get("data")?.Get("products")?.Get("edges")?.Get(0)?.Get("node");

var productIdString = node?.Get("id")?.GetString();
var originalSrcString = node?.Get("featuredImage")?.Get("originalSrc")?.GetString();
Int64? someIntegerValue = node?.Get("Size")?.GetInt64();  // You could use "var" here also, I used Int64? to make the inferred type explicit.

注意事项:

如果传入的元素不是预期的类型(对象或数组或空/缺失),上述扩展方法将引发异常。如果您不希望意外值类型出现异常,则可以放松对 ValueKind 的检查。

有一个开放的 API 增强请求Add JsonPath support to JsonDocument/JsonElement #31068。通过JSONPath 进行查询,如果实现的话,会让这类事情变得更容易。

如果您从 Newtonsoft 移植代码,请注意 JObject 会为缺少的属性返回 null,而 JArray 会抛出超出范围的索引。因此,在尝试模拟 Newtonsoft 的行为时,您可能希望直接使用 JElement 数组索引器,就像这样,因为它也会抛出越界的索引:

var node = doc.Get("data")?.Get("products")?.Get("edges")?[0].Get("node");

演示小提琴here.

【讨论】:

谢谢!今天一直在使用它,它为我节省了大量时间。【参考方案2】:

为了使我的代码更具可读性,我创建了一个方法,它使用 System.Text.Json 的点分隔路径,类似于 Newtonsoft.Json 中 SelectToken() 方法的路径参数。

JsonElement jsonElement = GetJsonElement(doc, "data.products.edges");

然后我使用jsonElement.ValueKind 来检查返回类型。

private static JsonElement GetJsonElement(JsonElement jsonElement, string path)

    if (jsonElement.ValueKind == JsonValueKind.Null ||
        jsonElement.ValueKind == JsonValueKind.Undefined)
    
        return default;
    

    string[] segments =
        path.Split(new[]  '.' , StringSplitOptions.RemoveEmptyEntries);

    for (int n = 0; n < segments.Length; n++)
    
        jsonElement = jsonElement.TryGetProperty(segments[n], out JsonElement value) ? value : default;

        if (jsonElement.ValueKind == JsonValueKind.Null ||
            jsonElement.ValueKind == JsonValueKind.Undefined)
        
            return default;
        
    

    return jsonElement;

我创建了另一个简单的方法来检索返回的 JsonElement 的值作为字符串。

private static string GetJsonElementValue(JsonElement jsonElement)

    return
        jsonElement.ValueKind != JsonValueKind.Null &&
        jsonElement.ValueKind != JsonValueKind.Undefined ?
        jsonElement.ToString() :
        default;

以下是应用于 OP 示例的两个函数:

public void Test()

    string raw = @"
        ""data"": 
        ""products"": 
            ""edges"": [
                
                    ""node"": 
                        ""id"": ""gid://shopify/Product/4534543543316"",
                        ""featuredImage"": 
                            ""originalSrc"": ""https://cdn.shopify.com/s/files/1/0286/pic.jpg"",
                            ""id"": ""gid://shopify/ProductImage/146345345339732""
                        
                    
                
            ]
        
        
    ";

    JsonElement doc = JsonSerializer.Deserialize<JsonElement>(raw);

    JsonElement jsonElementEdges = GetJsonElement(doc, "data.products.edges");

    string originalSrcString = default;
    string originalIdString = default;

    if (jsonElementEdges.ValueKind == JsonValueKind.Array)
    
        int index = 0; // Get the first element in the 'edges' array

        JsonElement edgesFirstElem =
            jsonElementEdges.EnumerateArray().ElementAtOrDefault(index);

        JsonElement jsonElement =
            GetJsonElement(edgesFirstElem, "node.featuredImage.originalSrc");
        originalSrcString = GetJsonElementValue(jsonElement);

        jsonElement =
            GetJsonElement(edgesFirstElem, "node.featuredImage.id");
        originalIdString = GetJsonElementValue(jsonElement);
    

    if (!string.IsNullOrEmpty(originalSrcString))
    
        // do stuff
    

    if (!string.IsNullOrEmpty(originalIdString))
    
        // do stuff
    

【讨论】:

【参考方案3】:

感谢Dave B 的好主意。我对其进行了改进,使其在访问数组元素时更高效,而无需编写太多代码。

string raw = @"
        ""data"": 
        ""products"": 
            ""edges"": [
                
                    ""node"": 
                        ""id"": ""gid://shopify/Product/4534543543316"",
                        ""featuredImage"": 
                            ""originalSrc"": ""https://cdn.shopify.com/s/files/1/0286/pic.jpg"",
                            ""id"": ""gid://shopify/ProductImage/146345345339732""
                        
                    
                ,
                
                    ""node"": 
                        ""id"": ""gid://shopify/Product/123456789"",
                        ""featuredImage"": 
                            ""originalSrc"": ""https://cdn.shopify.com/s/files/1/0286/pic.jpg"",
                            ""id"": [
                                ""gid://shopify/ProductImage/123456789"",
                                ""gid://shopify/ProductImage/666666666""
                            ]
                        ,
                        ""1"": 
                            ""name"": ""Tuanh""
                        
                    
                
            ]
        
        
    ";

使用也很简单

JsonElement doc = JsonSerializer.Deserialize<JsonElement>(raw);
JsonElement jsonElementEdges = doc.GetJsonElement("data.products.edges.1.node.1.name");



public static JsonElement GetJsonElement(this JsonElement jsonElement, string path)
        
            if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
                return default;

            string[] segments = path.Split(new[] '.', StringSplitOptions.RemoveEmptyEntries);

            foreach (var segment in segments)
            
                if (int.TryParse(segment, out var index) && jsonElement.ValueKind == JsonValueKind.Array)
                
                    jsonElement = jsonElement.EnumerateArray().ElementAtOrDefault(index);
                    if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
                        return default;

                    continue;
                

                jsonElement = jsonElement.TryGetProperty(segment, out var value) ? value : default;

                if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
                    return default;
            

            return jsonElement;
        

        public static string? GetJsonElementValue(this JsonElement jsonElement) => jsonElement.ValueKind != JsonValueKind.Null &&
                                                                                   jsonElement.ValueKind != JsonValueKind.Undefined
            ? jsonElement.ToString()
            : default;

【讨论】:

【参考方案4】:

我开发了一个名为 JsonEasyNavigation 的小型库,您可以在 github 或从 nuget.org 获取它。它允许您使用类似索引器的语法浏览 JSON 域对象模型:

var jsonDocument = JsonDocument.Parse(json);
var nav = jsonDocument.ToNavigation();

ToNavigation() 方法将 JsonDocument 转换为名为 JsonNavigationElement 的只读结构。它具有属性和数组项索引器,例如:

var item = nav["data"]["product"]["edges"][0];

然后您可以像这样检查实际存在的项目:

if (item.Exist)

   var id = item["id"].GetStringOrEmpty();
   // ...

我希望你会发现它有用。

【讨论】:

以上是关于使用 System.Text.Json 获取嵌套属性的主要内容,如果未能解决你的问题,请参考以下文章

System.Text.Json - 将嵌套对象反序列化为字符串

如何使用 System.Text.Json 序列化/反序列化非枚举类型的嵌套字典?

如何反序列化作为 System.Text.Json 中的字符串的嵌套 JSON 对象?

json.net 到 System.text.json 对 .net 5 中嵌套类的期望

System.Text.Json 反序列化来自 API 调用的嵌套对象 - 数据包装在父 JSON 属性中

使用 System.Text.Json 修改 JSON 文件