24

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.

langen
  • 740
  • 5
  • 17

3 Answers3

7

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; }
}
langen
  • 740
  • 5
  • 17
  • 8
    Personally I would rather use `= "";`, as otherwise you're basically saying (in my opinion) "This property is never going to be null (**just kidding**)". – Lasse V. Karlsen Oct 04 '19 at 09:32
  • 1
    Or use JSON.NET. System.Text.Json isn't a complete replacement *yet*. . As for `= default` ... not yet. When I read that in October 2019 I think `You are setting this to null!`. Maybe next year. – Panagiotis Kanavos Oct 04 '19 at 09:34
  • 1
    This is similar to what Microsoft recommends with entity objects. The EF team actually recommends using `null!` as a way of saying, 'I know this is never `null`.' Even though I like the potential of this nullability stuff, this sure feels a bit off... – Knelis Dec 10 '19 at 14:55
  • @LasseV.Karlsen Note that in the context of asp.net, adding `= null!` is actually the correct way to decorate a required property on an request object. It is the successor to the `[Required]` attribute. Adding `= null!` will simply tell the compiler that you know what you are doing, it will **not** allow the string to be deserialized to null. Since the string is non-nullable, asp.net will reject any requests that don't specify a value for the non-nullable/required reference type. Note that doing a `#nullable disable` will **actually** disable the null restriction -- so don't do that! – Matt Jacobi Apr 02 '20 at 20:26
  • 3
    @MattJacobi `System.Text.Json` will happily deserialize `null` into a non-nullable string property; at least by default, regardless of the `#nullable` status of the deserialized class (I just tried) - is there some options that triggers this stricter "I really mean nullability" mode? – Eamon Nerbonne Sep 10 '20 at 15:01
  • 2
    (both explicit null and implicit nulls by property ommission are not nullability errors by default for System.Text.Json) – Eamon Nerbonne Sep 10 '20 at 15:02
  • Just to echo what @EamonNerbonne said, System.Text.Json and even Json.Net, don't support nullable reference types. They will happily set a non-nullable string to null which is a pretty big gotcha. – LostInComputer Oct 17 '21 at 01:20
2

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...
benmccallum
  • 1,241
  • 12
  • 27
  • FYI, both System.Text.Json and Json.Net support parameterized constructors but they don't support nullable reference types. Using your example code, both deserializers will happy set the `Name` property to `null` which is pretty big gotcha. – LostInComputer Oct 17 '21 at 01:22
  • It's always a good idea to null check and throw in the ctor as you can't really trust others, good point. – benmccallum Oct 18 '21 at 14:03
  • Throwing an exception in the ctor causes ASP.net to return a 500 which is not a great. The ideal behavior is ASP.net returns a 4XX and tell the client what went wrong. – LostInComputer Oct 20 '21 at 02:15
  • To be fair, the original question wasn't really about model binding, which is always a bit painful. FWIW, ASP.NET Core 6 now considers nullability in model binding/validation and automatically returns a 400 Bad Request. Not sure if throwing in the ctor would cause a 500 still though. – benmccallum Oct 21 '21 at 15:28
0

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

Kyle McClellan
  • 664
  • 7
  • 23