Search code examples
c#c#-8.0nullable-reference-typesjsonnode

What is good form to write C# code in .net 8 to manipute a JsonNode object without compiler warnings about possible null-reference?


I'm working to become proficient with the newest C# language features and .net 8.

I understand that with C# 8.0 and above, the compiler will generate a warning when assigning to a reference type when the value being assigned might possibly be null unless the variable being assigned to is declared as a nullable reference type.

So, for example, I have a function that looks like this:

void ManipulateJson(string jsonString)
{
    JsonNode node = JsonNode.Parse(jsonString); // compiler warning
    string determinant = node["determinant"].GetValue<string>(); // compiler warning
    string id = newId(determinant);
    node["id"] = id;
}

The first two lines generate compiler warnings because JsonNode.Parse(node) and node["determinant"] may possibly be null.

If I understand correctly, I could write something like this:

void ManipulateJson(string jsonString)
{
    JsonNode? nodeNullable = JsonNode.Parse(jsonString);
    JsonNode node = nodeNullable ?? throw new Exception("null JSON value");
    JsonNode? determinantNodeNullable = node["determinant"];
    JsonNode determinantNode = determinantNodeNullable ?? throw new Exception("determinant is missing from JSON");
    string determinant = determinantNode.GetValue<string>(); // compiler warning
    string id = newId(determinant);
    node["id"] = id;
}

I could have avoided some of the noise above by using the null-forgiving operator, but that seems like it's defeating the point of nullable reference types.

What are the relevant best practices that would help me write code like this in good form for readability and maintainability, especially when I am manipulating more than one or two JSON properties?


Solution

  • First of all, you might want to know there is a very specific design in System.Text.Json that JSON null is represented as .NET null at runtime.

    For example:

    var json = "null";
    var node = JsonNode.Parse(json);
    Console.WriteLine(node is null); // Writes True
    

    If you keep this mind, the compiler warnings would make more sense and it should help you in deciding when to use null-forgiving operator.

    Now let's take a look at your code:

    JsonNode node = nodeNullable ?? throw new Exception("null JSON value");
    

    Whilst this fixes the compiler warning, it may or may not make sense at runtime. What if your input is null like in my example above? So you could decide to fix this line of code accordingly, that:

    • If your code never expects node to be null, you can throw exception
    • If your input is never null, you could forgive it using !

    For example:

    JsonNode node = JsonNode.Parse(jsonString)!;
    

    Remember that nullable reference is a compile time concern and it has no runtime meaning. You as the code writer knowns the input better than static code analysis tools. Having that said, if you decide to forgive the compiler warning and you indeed get a null at runtime, there will be a NullReferenceException thrown.

    The same theory applies to this line of your code as well:

    JsonNode? determinantNodeNullable = node["determinant"];
    

    In this context, I assume node is a JsonObject (returned by Parse method). A JsonObject is essentially a variant of Dictionary<string, JsonNode>. When you do node["determinant"], it looks up in the internal storage for the property node, and when it can't find it or when the node value is null, node["determinant"] returns null.

    var json = "{\"prop\": null}";
    var node = JsonNode.Parse(json)!;
            
    var prop1 = node["foo"];
    var prop2 = node["prop"];
    
    // Both write True
    Console.WriteLine(prop1 is null);
    Console.WriteLine(prop2 is null);