asp.net-coreasp.net-core-mvcmodel-bindingcustom-model-binderdefaultmodelbinder

ASP.NET Core MVC: removing validation errors for non-posted values in model binding/json conversion?


By default when you post to an ASP.NET Core MVC method, the model binder/json converter will validate any validation attributes or custom validation logic on the full model when converting from JSON to the model.

Is it possible to create a custom model binder, property binder, or json converter that would would cause the ModelState for the model to only have validation errors for properties of your model that were actually posted?

So if I had the following model (including validation logic/attributes):

public class PersonModel : IValidatableObject
{
    public Guid? Id { get; set; }

    [Required]
    [StringLength(50)]
    public string FirstName { get; set; } = string.Empty;

    [Required]
    [StringLength(50)]
    public string LastName { get; set; } = string.Empty;

    public int? Age { get; set; }

    [Required]
    [EmailAddress]
    [StringLength(100)]
    public string Email { get; set; } = string.Empty;

    [Required]
    [EmailAddress]
    [StringLength(100)]
    [Compare(nameof(Email))]
    public string EmailConfirm { get; set; } = string.Empty;

    [Required]
    [DataType(DataType.Date)]
    public DateOnly BirthDate { get; set;}

    public List<string> Hobbies = [];

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Hobbies.Count < 1) 
        {
            yield return new ValidationResult(
                "At least one hobby must be selected.",
                [nameof(Hobbies)]);
        }

        if (BirthDate == null)
        {
            yield return new ValidationResult(
               "Birthdate is required to cross check against age.",
               [nameof(BirthDate)]);
        }
        else
        {
            int yearsPassed = DateTime.Now.Year - ((DateOnly)BirthDate).Year;

            if (DateTime.Now.Month < ((DateOnly)BirthDate).Month || (DateTime.Now.Month == ((DateOnly)BirthDate).Month && DateTime.Now.Day < ((DateOnly)BirthDate).Day))
            {
                yearsPassed--;
            }

            if (yearsPassed != Age)
            {
                yield return new ValidationResult(
                    "Age does not match birthdate.",
                    [nameof(BirthDate), nameof(Age)]);
            }
        }
    }
}

And I post this:

{
   Id: null,
   FirstName: "Joe",
   LastName: "Bob",
   Age: null,
   Hobbies: []
}

I would not want the ModelState to include any validation messages for BirthDate since that value wasn't posted while still having the ModelState.Valid() equal to false.

How can I achieve this while keeping as much of the default MVC conventions as possible? For instance, I know I could just make all fields optional and write my own custom validation logic in my action, but I would like to avoid a solution like that as it forces you to do something that otherwise would not be advised (changing your model to make all properties impossible to fail validation on model binding).

I thought there may be something in ModelBindingContext and I could write a custom model binder or JSON converter, but I can't see anything in the API where things get added to the ModelState in the controller that I could override.


Solution

  • No custom model binder or JSON converter would work for this scenario as the Model Binder executes before the validation. The Binder is bound to the exact model type of your model, which is determined.

    You could see in your custom modelbinder that the ModelState is always valid as it is not validated yet. Thus there is no ErrorMessage could be removed.

    One workaround would be to generate a default value for every property if not posted , however it would cause new problems.