Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Null-coalescing operator returning null for properties of dynamic objects

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.

like image 658
James Ko Avatar asked Mar 14 '15 17:03

James Ko


People also ask

What is the use of null coalescing operator?

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.

Is there a null coalescing operator in JavaScript?

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.

Is coalesce null?

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.

What is null conditional and null coalescing in C#?

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.


1 Answers

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).

like image 130
dbc Avatar answered Sep 25 '22 11:09

dbc