How to use null conditional operator in a dynamic JSON with JsonConvert.DeserializeObject

879 Views Asked by At

I'm using Newtonsoft to deserialize an known JSON object and retrieve some values from it if they're present.

The crux is there is that object structure may keep changing so I'm using dynamic to traverse the structure and retrieve the values. Since the object structure keeps changing, I'm using the null conditional operator to traverse the JSON.

The code looks like this

dynamic jsonMeta = JsonConvert.DeserializeObject<dynamic>(jsonScript);
string gVal = jsonMeta.a?.b?.c?.d?.e?.f?.g?.Value ?? ""

The whole idea of this is to traverse the object in a null safe manner so that if a member doesn't exist, it evaluates to null and it assigns it a default value without throwing an exception. But what I'm seeing is that I get a exception 'Newtonsoft.Json.Linq.JValue' does not contain a definition for 'e' if that member d is null.

My understanding is that while the Value of d is null, it's of type JValue so that's why the null conditional operator doesn't work, but then it tries to access member e inside d it throws the exception.

So my question is how can I make this work in C#? Is there an easy way to access the JSON members without knowing the JSON structure in a single line or relatively easy way?

2

There are 2 best solutions below

1
On BEST ANSWER

Unfortunately due to the design limitation of NewtonSoft JSON.NET it cannot use null coalescing or null conditional operators when using it as a dynamic cast as I've shown in my question above.

One solution I found is to use System.Web.Helpers.Json, this implementation allows you to do what I'm trying to do above without running into the exception that JSON.NET throws because the way it evaluates the members at runtime leading to a simple way to access members of dynamic JSON structures. Plus you don't need to reference Value for a member, it's implicit.

using System.Web.Helpers.Json

dynamic jsonMeta = Json.Decode(jsonString);
string gVal = jsonMeta.a?.b?.c?.d?.e?.f?.g ?? ""

You however need to install the assemblies separately depending on which version of Visual Studio you're using (which is also required for JSON.NET). With VS 2019, it's very easy to install it with a single click through the IDE error assistant. More details about it here: Where can I find System.Web.Helpers, System.Web.WebPages, and System.Web.Razor?

The limitation of Json.Decode is that it cannot handle duplicate keys (throws an exception) and also has a maximum limit on then number of characters it can parse (MaxJsonLength error) which can be inconvenient to fix.

0
On

Another option is to use a dynamic ExpandoObject while deserializing the JSON with Newtonsoft.Json. This allows you to use the null conditional and null coalascing operators on the dynamic object.

dynamic jsonMeta = JsonConvert.DeserializeObject<ExpandoObject>(jsonScript, new ExpandoObjectConverter());
string gVal = jsonMeta.a?.b?.c?.d ?? ""

But this has a limitation that if it accesses an non-existent member the ExpandoObject will throw an exception (it is a sealed class and this behavior is hardcoded). So to work around this behavior I wrote a custom SafeExpandoObject class which wraps around the ExpandoObject and will allow you to access all/non-existent members without having an exception thrown, it will return null instead when you try to access a member that doesn't exist.

You can find the code here: https://codereview.stackexchange.com/questions/287243/wrapping-an-expandoobject-within-a-safeexpandoobject

Use it like this:

dynamic jsonMeta = JsonConvert.DeserializeObject<ExpandoObject>(jsonScript, new ExpandoObjectConverter());
jsonMeta = new SafeExpandoObject(jsonMeta); // Rewrap the ExpandoObject in a SafeExpandoObject that doesn't throw exceptions
string gVal = jsonMeta.a?.b?.c?.d ?? ""

Another point to note is that you cannot use direct indexing with ExpandoObject like jsonMeta["@name"].

To overcome this limitation you will need to either implement an IDictionary interface to the SafeExpandoObject to allow for direct indexing or cast the ExpandoObject it to a IDictionary<string, object> and then index it like

dynamic jsonMeta = JsonConvert.DeserializeObject<ExpandoObject>(jsonScript, new ExpandoObjectConverter());
string a = (jsonMeta as IDictionary<string, object>)["@name"];