DEV Community

evdbogaard
evdbogaard

Posted on • Originally published at evdbogaard.nl

Handling Nullable References with deserialization in .NET 9

.NET 9 introduced many features, including long-awaited improvements for JSON deserialization. These updates enhance handling nullable types, which helps avoid common issues like NullReferenceException.

Over the years, C# has provided tools to manage nullability, such as enabling nullable annotations and using the required modifier. However, deserialization often bypassed these safeguards. Let’s explore how .NET 9 addresses this problem.

Nullable reference types

Before C# 8.0, NullReferenceException errors were common, especially with uninitialized strings. A string without a value defaults to null, creating potential runtime issues. Developers often assigned default values to avoid exceptions, but these solutions lacked visibility.

With C# 8.0, nullable reference types were introduced to improve safety by detecting null references. Enabling this feature requires adding <Nullable>enable</Nullable> to your project file.

Once enabled, the compiler issues warnings about potentially uninitialized properties. For example, the class below triggers warnings for FirstName and LastName, as there is no constructor to initialize them:

class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

To resolve these warnings, you can provide a default value or mark the property as optional:

class Person
{
    public string FirstName { get; set; } = ""; // Default value ensures it's never null
    public string? LastName { get; set; } // Optional, can still be null
}
Enter fullscreen mode Exit fullscreen mode

The required modifier

Setting default values isn't always ideal. In C# 11, the required modifier was introduced to enforce property initialization during object creation. A required property must be set, or the code won’t compile.

Setting a default value isn't always what we want. In C# 11 the required modifier was introduced which allows us to say "you cannot create a new object of this class, without at least specifying these fields/properties".
It allows us to get rid of the default value and now prevents code compilation if a required property isn't being set.

class Person
{
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
}

// --- Example

var person = new Person
{
    FirstName = "John"
}; // Compilation error: 'LastName' is required
Enter fullscreen mode Exit fullscreen mode

This modifier ensures objects are properly initialized.

The problem with deserialization

By default, System.Text.Json ignores nullability when deserializing objects. Consider the example below:

class Person
{
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
}

// ---

var json = "{\"firstName\":\"John\", \"lastName\":null}";
var person = JsonSerializer.Deserialize<Person>(json, options: JsonSerializerOptions.Web);
Console.WriteLine($"Person: {person.FirstName} {person.LastName}"); // Output: "Person: John "
person.LastName.ToUpper(); // Throws NullReferenceException
Enter fullscreen mode Exit fullscreen mode

The deserializer creates a Person object with LastName set to null, even though this would not be possible with direct initialization. This behavior undermines the protections provided by nullable reference types and required modifiers.

Respecting Nullable Annotations in .NET 9

.NET 9 addresses this issue with the RespectNullableAnnotationsDefault feature. This opt-in feature enforces nullability rules during deserialization, ensuring invalid objects aren’t created. Since it introduces breaking changes, it’s disabled by default but recommended for new projects.

Here's the updated behavior:

class Person
{
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
}

// ---

var json = "{\"firstName\":\"John\", \"lastName\":null}";
var person = JsonSerializer.Deserialize<Person>(json, options: JsonSerializerOptions.Web);
// Throws JsonException: "The property or field 'lastName' on type 'Person' doesn't allow setting null values"
Enter fullscreen mode Exit fullscreen mode

Enabling RespectNullableAnnotationsDefault

To enable this feature globally, add the following to your project file:

<ItemGroup>
  <RuntimeHostConfigurationOption Include="System.Text.Json.Serialization.RespectNullableAnnotationsDefault" Value="true" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

Alternatively, enable it for specific calls:

JsonSerializerOptions options = new() { RespectNullableAnnotations = true };
JsonSerializer.Deserialize<Person>(json, options: options);
Enter fullscreen mode Exit fullscreen mode

Conclusion

With .NET 9's RespectNullableAnnotationsDefault option, developers can better enforce property validity during deserialization. These improvements complement existing nullability tools.

References

If you want to read more about changes to system.text.json in .NET 9 or nullable references in general, please check out the following links:

Top comments (0)