Vivian River
Vivian River

Reputation: 32400

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?

Upvotes: 1

Views: 486

Answers (2)

weichch
weichch

Reputation: 10055

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

Upvotes: 1

StriplingWarrior
StriplingWarrior

Reputation: 156708

It sounds like you have a pretty good handle on the most common idiomatic options:

  • If you're quite sure that the input won't have null/missing properties, you can use null-forgiveness operators, disable null-checking entirely for this section of code, or skip JsonNode completely and parse the string to a strong type.
  • If you're not so sure, it's probably best to use the ?? throw syntax, with a descriptive exception message to make it easy to track down what's missing.

Of course, you also have the full range of C# at your disposal to come up with your own custom options. For example, you could create custom extension methods that will try to get a property, and throw an exception with useful context information if it's not there.

public static class JsonNodeExtensions
{
    public static JsonNode GetRequiredProperty(this JsonNode node, string propertyName) =>
        node is not JsonObject obj ? throw new InvalidOperationException($"Unable to access property {propertyName}: JSON Path {node.GetPath()} is not an object")
        : obj[propertyName] ?? throw new InvalidOperationException($"JSON Path {node.GetPath()}.{propertyName} is null or undefined");
}

Upvotes: 0

Related Questions