【问题标题】:Getting nested properties with System.Text.Json使用 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
}

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

【问题讨论】:

    标签: c# .net-core system.text.json


    【解决方案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();
           // ...
        }
        

        我希望你会发现它有用。

        【讨论】:

          猜你喜欢
          • 2018-07-31
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-05-12
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多