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

16

我正在项目中使用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
}

代码量不算很多,但检查几个属性是非常常见的,我希望有一种更清晰、更易读的方法。

5个回答

29

您可以添加一些扩展方法,通过属性名或数组索引访问子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.

注意:

  • The extension methods above will throw an exception if the incoming element is not of the expected type (object or array or null/missing). You could loosen the checks on ValueKind if you never want an exception on an unexpected value type.

  • There is an open API enhancement request Add JsonPath support to JsonDocument/JsonElement #31068. Querying via JSONPath, if implemented, would make this sort of thing easier.

  • If you are porting code from Newtonsoft, be aware that JObject returns null for a missing property, while JArray throws on an index out of bounds. Thus you might want to use the JElement array indexer directly when trying to emulate Newtonsoft's behavior, like so, since it also throws on an index out of bounds:

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

这里有一个演示示例链接


谢谢!我今天一直在使用它,它为我节省了大量时间。 - Guerrilla
这太棒了,节省了很多时间! - Rob

6
为了使我的代码更易读,我创建了一个方法,它使用类似于Newtonsoft.Json中`SelectToken()`方法的路径参数,并使用System.Text.Json中的点分隔路径。
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
    }
}

5

感谢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的小型库,您可以从githubnuget.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();
   // ...
}

我希望这对您有所帮助。


.NET的Json真的需要额外的库来完成这样的事情吗?这应该是非常常见的吧?我的意思是,我一直在使用Newtonsoft Json,并考虑使用.NET的Json来减少外部库的数量,但如果我必须添加另一个库来完成这样的常见任务,那么这就打败了从Newtonsoft迁移的目的。我可能会继续使用Newtonsoft Json。 - Damn Vegetables

0

根据返回的JsonElement类型,您必须以不同的方式处理它。

我的情况是返回的元素是ValueKind = Array:"[[47.751]]" 因此,为了获取它,我创建了这个方法

private object GetValueFromJsonElement(WorkbookRange range)
{
    // The RootElement is the JsonElement
    var element = range.Values.RootElement.EnumerateArray().First()[0];
    switch (element.ValueKind)
    {
        case JsonValueKind.Number:
            return element.GetDouble();

        case JsonValueKind.String:
            return element.GetString();

        case JsonValueKind.True:
        case JsonValueKind.False:
            return element.GetBoolean();
        default:
            throw new InvalidOperationException("The Value Type returned is not handled");
    }
}Depending on the type of JsonElement  returned you have to handle it differently.

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