ASP.NET Core [Require] non-nullable types

Solution working with json requests

You cannot validate an already created model instance, because a non-nullable property has always a value (no matter whether it was assigned from json or is a default value). The solution is to report the missing value already during deserialization.

Create a contract resolver

public class RequiredPropertiesContractResolver : DefaultContractResolver
{
    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        var contract = base.CreateObjectContract(objectType);

        foreach (var contractProperty in contract.Properties)
        {
            if (contractProperty.PropertyType.IsValueType
                && contractProperty.AttributeProvider.GetAttributes(typeof(RequiredAttribute), inherit: true).Any())
            {
                contractProperty.Required = Required.Always;
            }
        }

        return contract;
    }
}

and then assign it to SerializerSettings:

services.AddMvc()
        .AddJsonOptions(jsonOptions =>
        {
            jsonOptions.SerializerSettings.ContractResolver = new RequiredPropertiesContractResolver();
        });

The ModelState is then invalid for non-nullable properties with the [Required] attribute if the value is missing from json.


Example

Json body

var jsonBody = @"{ Data2=123 }"

is invalid for model

class Model
{
    [Required]
    public int Data { get; set; }

    public int Data2 { get; set; }
}

Everything from the request is just a string. The modelbinder matches up keys in the request body with property names, and then attempts to coerce them to the appropriate type. If the property is not posted or is posted with an empty string, that will obviously fail when trying to convert to an int. As a result, you end up with the default value for the type. In the case of an int that's 0, while the default value of int? is null.

Only after this binding process is complete is the model then validated. Remember you're validating the model not the post body. There's no reasonable way to validate the post body, since again, it's just a a bunch of key-value pair strings. Therefore, in the case of an int property that's required, but not posted, the value is 0, which is a perfectly valid value for an int, and the validation is satisfied. In the case of int?, the value is null, which is not a valid int, and thus fails validation. That is why the nullable is required, if you want to require a non-nullable type have a value. It's the only way that an empty value can be differentiated from simply a "default" value.

If you are using view models, as you should be, this should not be an issue. You can bind to a nullable int with a required attribute, and you will be assured that it will have a value, despite being nullable, if your model state is valid. Then, you can map that over to a straight int on your entity. That is the correct way to handle things.