Getting rid of warnings with nullable reference types and JSON object models in C#

Edit on GitHub

In my blog series, Nullable reference types in C# - Migrating to nullable reference types, we discussed the benefits of enabling nullable reference types in your C# code, and annotating your code so the compiler and IDE can give you more reliable hints about whether a particular variable or property may need to be checked for being null before using it.

We ended the series with a curious case: how to annotate classes to deserialize JSON.

The issue is this: you’ll typically have several Data Transfer Objects (DTO)/Plain-Old CLR Objects (POCO) in your project that declare properties to deserialize the data into. You know for sure the data will be there after deserializing, so you declare these properties as non-nullable. Yet, the compiler (and IDE) insist on you either making it a nullable property or initializing the property.

Non-nullable property is uninitialized. Consider declaring the property as nullable.

How to go about that? There are several options, each with their own advantages and caveats. Let’s have a look.

Option 1: Make the property nullable

If you follow the compiler’s advice, you can update the property and make it nullable:

public class User
{
    [JsonProperty("name")]
    public string? Name { get; set; }
}

This will get rid of the warning, but you now have to check the Name property for potential null values everywhere it is used. If the JSON may contain null values, this is a great approach. However, when you know for sure there will always be a value, it adds a lot of overhead in your codebase.

Option 2: Add a default! (please don’t)

You could also keep the property as non-nullable, and initialize the property with default!. This effectively sets the default value to null but suppresses the warning.

public class User
{
    [JsonProperty("name")]
    public string Name { get; set; } = default!;
}

I highly recommend against doing this. If the deserialized JSON does not contain a value for the Name property, it will now hold a null value. The compiler and IDE are satisfied and will no longer warn you about this, meaning unexpected NullReferenceException may be thrown at runtime.

The goal of nullable reference types/nullable annotations is to provide you with a null safety net, and the above is sabotaging that safety net from the start.

Option 3: Add a primary constructor

If you’re using Newtonsoft.Json as your JSON framework of choice, you can add a primary constructor to your class that sets all non-nullable properties.

The JSON deserializer will pick this up, and calls the constructor instead of setting the properties directly:

public class User
{
    public User(string? name)
    {
        Name = name ?? "Unknown"; // or ArgumentNullException.ThrowIfNull(name)
    }

    [JsonProperty("name")]
    public string Name { get; init; }
}

What’s nice with this approach is that the nullability warning will be gone, and you’re modeling your C# representation very closely to the JSON you want to deserialize. If you’re certain no null will be in the JSON, a non-nullable property in C# makes sense.

In addition, you can either set a default value or throw an ArgumentNullException in the constructor. The last option may mean you’ll see an exception at runtime, but then that exception is there because the JSON data is not what you expected, and other action may be needed (such as logging an incident) instead of happily continuing to run your code.

Option 4: Annotations and default values

Instead of setting the property to default and suppressing the nullability warning, you can also set a proper default value. In the following example, the Name property is non-nullable and contains an expected default value when no value is deserialized from JSON:

public class User
{
    [JsonProperty("name")]
    public string Name { get; init; } = "Unknown";
}

If you’re using record classes, you can do this as well:

public record User(
    [property: JsonProperty("name")]
    string Name = "Unknown"
);

This is a really nice way to express classes that are just a representation of a JSON document.

Option 5: Use a required property

In C# 11, the required modifier was added as a way to indicate that a field or property must be initialized by all constructors or by using an object initializer.

Given the compiler expects the property to always be initialized and contain a value, this means the nullability warning is no longer there. It helps make sure your own code always has to initialize such properties, and that it’s safe to assume no null reference will be present at runtime.

public class User
{
    [JsonProperty("name")]
    public required string Name { get; set; }
}

Personally, I like this approach the most. It clearly sets expectations, without providing the compiler and IDE with false information.

Do keep in mind it is important that the JSON document you are deserializing always contains a value and is not null. The required modifier is enforced at compile time, and not at runtime. If a null reference is set by the JSON framework you are using, there’s no guarantee NullReferenceException can’t occur.

If you expect null in some cases, annotating the property as nullable (string?) and performing null checks where applicable is the recommended approach.

Leave a Comment

avatar

5 responses

  1. Avatar for Alf Kåre Lefdal
    Alf Kåre Lefdal January 16th, 2023

    Option 6: Use two different types, one DTO that is easy to serialize and deserialize, and one proper domain object. With functions to map between them. See Einar Høst’s talk from NDC Oslo 2022 on Youtube: How I work with JSON.

  2. Avatar for Jack
    Jack January 16th, 2023

    I like to handle this scenario by using a naming convention + adding a pattern match rule into .editorconfig so that we don’t get warnings on these types of records

  3. Avatar for soundos
    soundos March 13th, 2023

    really nice post. thanks for sharing beautiful content.

  4. Avatar for Mattias Nordqvist
    Mattias Nordqvist April 1st, 2023

    How is option 5 different from 2 except for the fact that it is more expressive? The default value will still be null when deserializing, right?

  5. Avatar for Maarten Balliauw
    Maarten Balliauw April 3rd, 2023

    Correct. This is assuming the C# code and JSON are both correctly annotated/generated, and the JSON never contains a null where the POCO says otherwise.