How to differentiate between null and non existing data in JSON in Asp.Net Core model binding?

Just to add another 2 cents, we went the similar way to the Ilya's answer, except that we're not calling SetHasProperty from setter, but overriding DefaultContractResolver:

public class PatchRequestContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var prop = base.CreateProperty(member, memberSerialization);

        prop.SetIsSpecified += (o, o1) =>
        {
            if (o is PatchRequest patchRequest)
            {
                patchRequest.SetHasProperty(prop.PropertyName);
            }
        };

        return prop;
    }
}

And then register this resolver in Startup:

services
    .AddControllers()
    .AddNewtonsoftJson(settings =>
        settings.SerializerSettings.ContractResolver = new PatchRequestContractResolver());```

Note, that we are still using JSON.Net and not the System.Text.Json (which is default for .Net 3+) for deserializing. As of now there's no way to do things similar to DefaultContractResolver with System.Text.Json


This is what I ended up doing, as all other options seem to be too complicated (e.g. jsonpatch, model binding) or would not give the flexibility I want.

This solution means there is a bit of a boilerplate to write for each property, but not too much:

public class UpdateRequest : PatchRequest
{
    [MaxLength(80)]
    [NotNullOrWhiteSpaceIfSet]
    public string Name
    {
       get => _name;
       set { _name = value; SetHasProperty(nameof(Name)); }
    }  
}

public abstract class PatchRequest
{
    private readonly HashSet<string> _properties = new HashSet<string>();

    public bool HasProperty(string propertyName) => _properties.Contains(propertyName);

    protected void SetHasProperty(string propertyName) => _properties.Add(propertyName);
}

The value can then be read like this:

if (request.HasProperty(nameof(request.Name)) { /* do something with request.Name */ }

and this is how it can be validated with a custom attribute:

var patchRequest = (PatchRequest) validationContext.ObjectInstance;
if (patchRequest.HasProperty(validationContext.MemberName) {/* do validation*/}