Getting nested properties with System.Text.Json

Gue*_*lla 10 c# .net-core system.text.json

I am working with System.Text.Json in my project as I am processing large files so also decided to use it for processing GraphQL responses.

Due to the nature of GraphQL sometimes I get highly nested responses that are not fixed and don't make sense to map to a class. I usually need to check a few properties on the response.

My issue is with JsonElement. To check nested properties feels very clumsy and I feel like there should be a better way to approach this.

For example take my below code simulating a response I get. I just want to check if 2 properties exist (id & originalSrc) and if they do get their value but it feels like I have made a meal of the code. Is there a better/clearer/more succinct way to write this?

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
}
Run Code Online (Sandbox Code Playgroud)

It is not a crazy amount of code but checking a handful of properties is so common I would like a cleaner more readble approach.

dbc*_*dbc 15

您可以添加几个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;
        var value = element.EnumerateArray().ElementAtOrDefault(index);
        return value.ValueKind != JsonValueKind.Undefined ? value : (JsonElement?)null;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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.
Run Code Online (Sandbox Code Playgroud)

笔记:

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

  • 有一个开放的 API 增强请求Add JsonPath support to JsonDocument/JsonElement #31068。如果实现,通过JSONPath查询将使这种事情变得更容易。

演示小提琴在这里


Dav*_*e B 6

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

JsonElement jsonElement = GetJsonElement(doc, "data.products.edges");
Run Code Online (Sandbox Code Playgroud)

然后我用它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;
}
Run Code Online (Sandbox Code Playgroud)

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

private static string GetJsonElementValue(JsonElement jsonElement)
{
    return
        jsonElement.ValueKind != JsonValueKind.Null &&
        jsonElement.ValueKind != JsonValueKind.Undefined ?
        jsonElement.ToString() :
        default;
}
Run Code Online (Sandbox Code Playgroud)

以下是应用于 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
    }
}
Run Code Online (Sandbox Code Playgroud)


小智 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""
                        }
                    }
                }
            ]
        }
        }
    }";
Run Code Online (Sandbox Code Playgroud)

使用方法也相当简单

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;
Run Code Online (Sandbox Code Playgroud)