skip to Main Content

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
}

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.

5

Answers


  1. You could add a couple of extension methods that access a child JsonElement value by property name or array index, returning a nullable value if not found:

    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;
        }
    }
    

    Now calls to access nested values can be chained together using the null-conditional operator ?.:

    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.
    

    Notes:

    • 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");
      

    Demo fiddle here.

    Login or Signup to reply.
  2. To make my code a little more readable I created a method that uses a dot-separated path with System.Text.Json similar to a path parameter for the SelectToken() method in Newtonsoft.Json.

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

    I then use jsonElement.ValueKind to check the return type.

    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;
    }
    

    I created another simple method to retrieve the value of the returned JsonElement as a string.

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

    Below are two functions applied to the OP’s sample:

    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
        }
    }
    
    Login or Signup to reply.
  3. Thank Dave B for a good idea. I have improved it to be more efficient when accessing array elements without having to write too much code.

    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""
                            }
                        }
                    }
                ]
            }
            }
        }";
    

    Usage is also quite simple

    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;
    
    Login or Signup to reply.
  4. I have developed a small library named JsonEasyNavigation, you can get it on github or from nuget.org. It allows you to navigate through JSON Domain Object Model using indexer-like syntax:

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

    ToNavigation() method converts JsonDocument into readonly struct named JsonNavigationElement. It has property and array item indexers, for example:

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

    Then you can check for actual items existince like this:

    if (item.Exist)
    {
       var id = item["id"].GetStringOrEmpty();
       // ...
    }
    

    I hope you will find it useful.

    Login or Signup to reply.
  5. Depending on the type of JsonElement returned you have to handle it differently.

    My case was that the returned element was ValueKind = Array : "[[47.751]]"
    So in order to get it I did created this method

    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.
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search