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
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:Now calls to access nested values can be chained together using the null-conditional operator
?.
: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
returnsnull
for a missing property, whileJArray
throws on an index out of bounds. Thus you might want to use theJElement
array indexer directly when trying to emulate Newtonsoft’s behavior, like so, since it also throws on an index out of bounds:Demo fiddle here.
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.I then use
jsonElement.ValueKind
to check the return type.I created another simple method to retrieve the value of the returned
JsonElement
as a string.Below are two functions applied to the OP’s sample:
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.
Usage is also quite simple
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:
ToNavigation() method converts JsonDocument into readonly struct named JsonNavigationElement. It has property and array item indexers, for example:
Then you can check for actual items existince like this:
I hope you will find it useful.
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