Respect nullable annotations

Starting in .NET 9, JsonSerializer has (limited) support for non-nullable reference type enforcement in both serialization and deserialization. You can toggle this support using the JsonSerializerOptions.RespectNullableAnnotations flag.

For example, the following code snippet throws a JsonException during serialization with a message like:

The property or field 'Name' on type 'Person' doesn't allow getting null values. Consider updating its nullability annotation.

    public static void RunIt()
    {
#nullable enable
        JsonSerializerOptions options = new()
        {
            RespectNullableAnnotations = true
        };

        Person invalidValue = new(Name: null!);
        JsonSerializer.Serialize(invalidValue, options);
    }

    record Person(string Name);

Similarly, RespectNullableAnnotations enforces nullability on deserialization. The following code snippet throws a JsonException during serialization with a message like:

The constructor parameter 'Name' on type 'Person' doesn't allow null values. Consider updating its nullability annotation.

    public static void RunIt()
    {
#nullable enable
        JsonSerializerOptions options = new()
        {
            RespectNullableAnnotations = true
        };

        string json = """{"Name":null}""";
        JsonSerializer.Deserialize<Person>(json, options);
    }

    record Person(string Name);

Tip

Limitations

Due to how non-nullable reference types are implemented, this feature comes with some important limitations. Familiarize yourself with these limitations before turning the feature on. The root of the issue is that reference type nullability has no first-class representation in intermediate language (IL). As such, the expressions MyPoco and MyPoco? are indistinguishable from the perspective of run-time reflection. While the compiler tries to make up for that by emitting attribute metadata (see sharplab.io example), this metadata is restricted to non-generic member annotations that are scoped to a particular type definition. This limitation is the reason that the flag only validates nullability annotations that are present on non-generic properties, fields, and constructor parameters. System.Text.Json does not support nullability enforcement on:

  • Top-level types, or the type that's passed when making the first JsonSerializer.Deserialize() or JsonSerializer.Serialize() call.
  • Collection element types—for example, the List<string> and List<string?> types are indistinguishable.
  • Any properties, fields, or constructor parameters that are generic.

If you want to add nullability enforcement in these cases, either model your type to be a struct (since they don't admit null values), or author a custom converter that overrides its HandleNull property to true.

Feature switch

You can turn on the RespectNullableAnnotations setting globally using the System.Text.Json.Serialization.RespectNullableAnnotationsDefault feature switch. Add the following MSBuild item to your project file (for example, .csproj file):

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

The RespectNullableAnnotationsDefault API was implemented as an opt-in flag in .NET 9 to avoid breaking existing applications. If you're writing a new application, it's highly recommended that you enable this flag in your code.

Relationship between nullable and optional parameters

RespectNullableAnnotations doesn't extend enforcement to unspecified JSON values, because System.Text.Json treats required and non-nullable properties as orthogonal concepts. For example, the following code snippet doesn't throw an exception during deserialization:

public static void RunIt()
{
    JsonSerializerOptions options = new()
    {
        RespectNullableAnnotations = true
    };
    var result = JsonSerializer.Deserialize<MyPoco>("{}", options);
    Console.WriteLine(result.Name is null); // True.
}

class MyPoco
{
    public string Name { get; set; }
}

This behavior stems from the C# language itself, where you can have required properties that are nullable:

MyPoco poco = new() { Value = null }; // No compiler warnings.

class MyPoco
{
    public required string? Value { get; set; }
}

And you can also have optional properties that are non-nullable:

class MyPoco
{
    public string Value { get; set; } = "default";
}

The same orthogonality applies to constructor parameters:

record MyPoco(
    string RequiredNonNullable,
    string? RequiredNullable,
    string OptionalNonNullable = "default",
    string? OptionalNullable = "default"
    );

See also