Handling nullability and required properties in .NET API models
TL;DR You can use either the required
keyword or set a default value to null!
when marking mandatory properties in your API models with nullable context enabled. If you send request without required
property, you get model binding error. If you send request without null!
property, you get model validation error, because of implicitly added [Required]
attribute.
Problems with API models in nullability context
When designing a model for a request in an API project, should I mark mandatory property as non-nullable or nullable?
- Nullable, because when client sends incomplete data, you can still receive null values in incoming request.
- Non-nullable, because it clearly specifies which properties are required, making it easier for clients to understand which fields must be provided. However, despite marking properties as non-nullable, you still need to check for nulls which can be confusing for developers ("Why do I check if non-nullable property is null?").
In my opinion, it's generally better to mark mandatory properties as non-nullable and perform validation checks, rather than making them nullable.
Required properties in your API model
You can use either the required
keyword or set a default value to null!
to handle mandatory properties in your API contracts.
The required
keyword is a new feature introduced in C# 11 and .NET 7 that allows you to specify that a property must be set during object initialization. If you are writing class library with API contracts and target netstandard2.0
(C# 7.3 by default), you need to use null!
.
public class SaveProfileRequest
{
public required string Username { get; set; }
public string Password { get; set; } = null!;
}
Sending request with missing required property
If we send request without Username
, we get automatic HTTP 400 response, because of model binding error (JSON deserialization error since required
property is missing).
Automatic HTTP 400 responses are enabled by default.
The [ApiController]
attribute makes model validation errors automatically trigger an HTTP 400 response.
In other words, you don't have to check if (!ModelState.IsValid)
in your action method.
See how you can disable them.
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"$": [
"JSON deserialization for type 'Api.SaveProfileRequest' was missing required properties, including the following: username"
],
"request": [
"The request field is required."
]
},
"traceId": "00-a21e45d9420d180dd9e4ef54939b66fc-c7b2dd90d41112c2-00"
}
The request itself becomes null
here:
[HttpPost]
public void SaveProfile([FromBody] SaveProfileRequest request)
{
// request is null here.
}
The required
modifier behaves as [JsonRequired]
for JSON deserialization.
Sending request with missing null! property
If we send request without Password
, we get automatic HTTP 400 response, because of model validation error.
Model state represents errors that come from two subsystems: model binding and model validation.
Model validation occurs after model binding.
Both model binding and model validation occur before the execution of a controller action.
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Password": [
"The Password field is required."
]
},
"traceId": "00-sdced1sd769cad82df2e5ddef4d9c8fc-ad5b5f7b564e4dea-00"
}
The request as a whole is not null
like in the previous case. Only the Password
property is.
After all, we set the default value to null
and lie to the compiler it's definitely not null with ! (null-forgiving) operator.
[HttpPost]
public void SaveProfile([FromBody] SaveProfileRequest request)
{
// request is not null here.
// request.Password is null here.
}
Why do we get validation error though?
It turns out, by enabling nullable contexts (the app is built with <Nullable>enable</Nullable>
), non-nullable properties are implicitly attributed with the [Required(AllowEmptyStrings = true)]
attribute.
This behavior can be disabled by configuring SuppressImplicitRequiredAttributeForNonNullableReferenceTypes
:
// Program.cs
builder.Services.AddControllers(options =>
options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true
);