I have recently found a problem with the null-coalescing operator while using Json.NET to parse JSON as dynamic objects. Suppose this is my dynamic object:
string json = "{ \"phones\": { \"personal\": null }, \"birthday\": null }";
dynamic d = JsonConvert.DeserializeObject(json);
If I try to use the ?? operator on one of the field of d, it returns null:
string s = "";
s += (d.phones.personal ?? "default");
Console.WriteLine(s + " " + s.Length); //outputs 0
However, if I assign a the dynamic property to a string, then it works fine:
string ss = d.phones.personal;
string s = "";
s += (ss ?? "default");
Console.WriteLine(s + " " + s.Length); //outputs default 7
Finally, when I output Console.WriteLine(d.phones.personal == null)
it outputs True
.
I have made an extensive test of these issues on Pastebin.
The null-coalescing operator ?? returns the value of its left-hand operand if it isn't null ; otherwise, it evaluates the right-hand operand and returns its result.
JavaScript made sure this can be handled with its nullish operator also known as the Null Coalescing Operator, which was added to the language with ECMAScript 2020. With it, you can either return a value or assign it to some other value, depending on a boolean expression.
The COALESCE function returns NULL if all arguments are NULL . The following statement returns 1 because 1 is the first non-NULL argument. The following statement returns Not NULL because it is the first string argument that does not evaluate to NULL . you will get the division by zero error.
it is used to define a default value for nullable value types or reference types. It returns the left-hand operand if the operand is not null; otherwise, it returns the right operand. In cases where a statement could return null, the null-coalescing operator can be used to ensure a reasonable value gets returned.
This is due to obscure behaviors of Json.NET and the ??
operator.
Firstly, when you deserialize JSON to a dynamic
object, what is actually returned is a subclass of the Linq-to-JSON type JToken
(e.g. JObject
or JValue
) which has a custom implementation of IDynamicMetaObjectProvider
. I.e.
dynamic d1 = JsonConvert.DeserializeObject(json);
var d2 = JsonConvert.DeserializeObject<JObject>(json);
Are actually returning the same thing. So, for your JSON string, if I do
var s1 = JsonConvert.DeserializeObject<JObject>(json)["phones"]["personal"];
var s2 = JsonConvert.DeserializeObject<dynamic>(json).phones.personal;
Both these expressions evaluate to exactly the same returned dynamic object. But what object is returned? That gets us to the second obscure behavior of Json.NET: rather than representing null values with null
pointers, it represents then with a special JValue
with JValue.Type
equal to JTokenType.Null
. Thus if I do:
WriteTypeAndValue(s1, "s1");
WriteTypeAndValue(s2, "s2");
The console output is:
"s1": Newtonsoft.Json.Linq.JValue: ""
"s2": Newtonsoft.Json.Linq.JValue: ""
I.e. these objects are not null, they are allocated POCOs, and their ToString()
returns an empty string.
But, what happens when we assign that dynamic type to a string?
string tmp;
WriteTypeAndValue(tmp = s2, "tmp = s2");
Prints:
"tmp = s2": System.String: null value
Why the difference? It is because the DynamicMetaObject
returned by JValue
to resolve the conversion of the dynamic type to string eventually calls ConvertUtils.Convert(value, CultureInfo.InvariantCulture, binder.Type)
which eventually returns null
for a JTokenType.Null
value, which is the same logic performed by the explicit cast to string avoiding all uses of dynamic
:
WriteTypeAndValue((string)JsonConvert.DeserializeObject<JObject>(json)["phones"]["personal"], "Linq-to-JSON with cast");
// Prints "Linq-to-JSON with cast": System.String: null value
WriteTypeAndValue(JsonConvert.DeserializeObject<JObject>(json)["phones"]["personal"], "Linq-to-JSON without cast");
// Prints "Linq-to-JSON without cast": Newtonsoft.Json.Linq.JValue: ""
Now, to the actual question. As husterk noted the ?? operator returns dynamic
when one of the two operands is dynamic
, so d.phones.personal ?? "default"
does not attempt to perform a type conversion, thus the return is a JValue
:
dynamic d = JsonConvert.DeserializeObject<dynamic>(json);
WriteTypeAndValue((d.phones.personal ?? "default"), "d.phones.personal ?? \"default\"");
// Prints "(d.phones.personal ?? "default")": Newtonsoft.Json.Linq.JValue: ""
But if we invoke Json.NET's type conversion to string by assigning the dynamic return to a string, then the converter will kick in and return an actual null pointer after the coalescing operator has done its work and returned a non-null JValue
:
string tmp;
WriteTypeAndValue(tmp = (d.phones.personal ?? "default"), "tmp = (d.phones.personal ?? \"default\")");
// Prints "tmp = (d.phones.personal ?? "default")": System.String: null value
This explains the difference you are seeing.
To avoid this behavior, force the conversion from dynamic to string before the coalescing operator is applied:
s += ((string)d.phones.personal ?? "default");
Finally, the helper method to write the type and value to the console:
public static void WriteTypeAndValue<T>(T value, string prefix = null)
{
prefix = string.IsNullOrEmpty(prefix) ? null : "\""+prefix+"\": ";
Type type;
try
{
type = value.GetType();
}
catch (NullReferenceException)
{
Console.WriteLine(string.Format("{0} {1}: null value", prefix, typeof(T).FullName));
return;
}
Console.WriteLine(string.Format("{0} {1}: \"{2}\"", prefix, type.FullName, value));
}
(As an aside, the existence of the null-type JValue
explains how the expression (object)(JValue)(string)null == (object)(JValue)null
might possibly evaluate to false
).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With