langen
langen

Reputation: 770

How to deal with nullable reference types with System.Text.Json?

I have upgraded my project to netcore 3.0 and I am in the middle of refactoring a project to use the new nullable references types feature, but got stuck pretty quickly because of the following issue.

Lets say I consume a REST api which returns the following JSON:

{
  "Name": "Volvo 240",
  "Year": 1989
}

This api always returns the name/year, so they are non-nullable.

I would use this simple class for deserialization:

public class Car
{
    public string Name {get; set;}
    public int Year {get; set;}
}

And I would deserialize this to a Car instance using the new System.Text.Json

var car = JsonSerializer.Deserialize<Car>(json);

This all works, but when enabling nullable reference types I get a warning in the Car class that Name is declared as non-nullable but can be null. I understand why I get this since it is possible to instantiate this object without initializing the Name property.

So ideally Car should look like this:

public class Car
{
    public string Name { get; }
    public int Year { get; }

    public Car(string name, int year)
    {
        Name = name;
        Year = year;
    }
}

But this doesn't work because System.Text.Json serializer doesn't support constructors with parameters.

So my question is: How would I declare Car so that Name is non-nullable and get it to work with System.Text.Json without getting "non-nullable" warning?`

I don't want to make it nullable because I would have to do null-checks on basically everything when enabling nullable reference types, and since the REST API in my example says that they are always provided they shouldn't be nullable.

Upvotes: 27

Views: 13197

Answers (3)

benmccallum
benmccallum

Reputation: 1378

Update If you're on net5, use the parameterized constructor support now offer as @langen points out. Else below can still be useful.

Original Slightly alternative approach. System.Text.Json appears to have no problems using a private parameterless constructor. So you can at least do the following:

public class Car
{
    public string Name { get; set; }
    public int Year { get; set; }

    // For System.Text.Json deserialization only
    #pragma warning disable CS8618 // Non-nullable field is uninitialized.
    private Car() { }
    #pragma warning restore CS8618

    public Car(string name, int year)
    {
        Name = name
            ?? throw new ArgumentNullException(nameof(name));
        Year = year;
    }
}

Benefits being:

  • Init of the object from your own code must be through the public ctor.
  • You don't need to do = null!; on each property.

Remaining downside with S.T.Json and nullable reference types:

  • S.T.Json still requires setters on the properties to actually set the values during deserialization. I tried with private ones and it's a no go, so we still can't get an immutable object...

Upvotes: 2

Kyle McClellan
Kyle McClellan

Reputation: 985

Another option, for those who want to handle missing properties with meaningful exceptions:

using System;

public class Car
{
    private string? name;
    private int? year;

    public string Name
    {
        get => this.name ?? throw new InvalidOperationException($"{nameof(this.Name)} was not set.");
        set => this.name = value;
    }

    public int Year
    {
        get => this.year ?? throw new InvalidOperationException($"{nameof(this.Year)} was not set.");
        set => this.year = value;
    }
}

Upvotes: 0

langen
langen

Reputation: 770

UPDATE

System.Text.Json for .NET 5 now supports parameterized constructors, so this should not be a problem anymore.

See https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-immutability?pivots=dotnet-5-0

Old answer below

After reading the msdocs I found out how I could solve this issue.

So until System.Text.Json cannot instantiate classes with parameters in their constructor, the Car class will have to look like this:

public class Car
{
    public string Name { get; set; } = default!;
    public int Year { get; set; }
}

Upvotes: 7

Related Questions