ModelStateDictionary - Identify errors that should not be returned to caller

727 views Asked by At

I have a simple web API that performs some basic validation using validation attributes. If the request does not meet the validation requirements, I then extract the error messages from the ModelStateDictionary and return these to the caller as a dictionary, where the key is the path to the property that has an error and the message indicates what the problem is.

The problem is that, given certain input's, the error messages in the ModelStateDictionary often contains information that I would not want to return to the client (such as the full name (including namespace) of the object that the converter attempted to map the JSON to).

Is it possible to differentiate between validation errors (generated by Validation Attributes / IValidatableObject implementations) and errors generated by attempting to map invalid JSON to a certain object?

For example:

My models

public class Order
{
    [Required]
    public Customer Customer { get; set; }
}

public class Customer
{
    [Required]
    public string Name { get; set; }
}

My action method

public IActionResult Post(Order order)
{
    if (!ModelState.IsValid)
    {
        var errors = GetErrors(ModelState);
                
        return BadRequest(errors);
    }

    return Ok();
}

private Dictionary<string, string> GetErrors(ModelStateDictionary modelState)
{
    var errors = new Dictionary<string, string>();
            
    foreach (var error in modelState)
    {
        string message = null;
        if (error.Value.Errors.Any(e => e.Exception != null))
        {
            message = "Unable to interpret JSON value."; 
        }
        else
        {
            message = string.Join(". ", string.Join(". ", error.Value.Errors.Select(e => e.ErrorMessage)));
        }
                
        errors.Add(error.Key, message);
    }

    return errors;
}

My example inputs:

  1. Missing required data
{
    "Customer": {
        "Name": null // name is required
    }
}

Because Customer.Name is decorated with the RequiredAttribute, this generates an error message of:

{
    "Customer.Name": "The Name field is required."
}

This is something that's OK to return to the caller, so no issues here.

  1. JSON value doesnt map to .Net object type
{
    "Customer": 123 // deserializing will fail to map this value to a Customer object
}

Because deserializing the value 123 to a Customer object fails, this generates an error message of:

{
    "Customer": "Error converting value 123 to type 'MyProject.Models.Customer'. Path 'Customer', line 2, position 19."
}

This is not OK to return to the caller, as it contains the full namespace path to the object being mapped to. In this case, something generic (such as "Bad JSON value.") should be used as the error message.

Can anyone help me find a solution to hide these error messages which contain information that should not be returned to the caller?


As can be seen in my code above, I thought I might be able to check the ModelEntry.Exception property and use this to determine whether the error message needs to be shielded from the caller, but this is null in both my examples.

On solution may be to check if the error message starts with Error converting value, and if so, shield the message from the caller. This doesnt seem very robust, and I'm not sure how reliable this will be in a real-world example.

2

There are 2 answers

1
Bryan Lewis On

Typically when model binding fails because the JSON payload is not properly formatted, then the incoming model object ("order" in your case) will be null. Just put in a simple null check and return BadRequest with a generic error message.

if (order == null)
    return BadRequest("Invalid JSON");
2
Rena On

The default response type for HTTP 400 responses is ValidationProblemDetails class. So, we will create a custom class which inherits ValidationProblemDetails class and define our custom error messages.

public class CustomBadRequest : ValidationProblemDetails
{
    public CustomBadRequest(ActionContext context)
    {
        ConstructErrorMessages(context);
        Type = context.HttpContext.TraceIdentifier;
    }

    private void ConstructErrorMessages(ActionContext context)
    {
       //this is the error message you get...
        var myerror = "Error converting value";
        foreach (var keyModelStatePair in context.ModelState)
        {
            var key = keyModelStatePair.Key;
            var errors = keyModelStatePair.Value.Errors;
            if (errors != null && errors.Count > 0)
            {
                if (errors.Count == 1)
                {
                    var errorMessage = GetErrorMessage(errors[0]);
                    if (errorMessage.StartsWith(myerror))
                    {
                        Errors.Add(key, new[] { "Cannot deserialize" });
                    }
                    else
                    {
                        Errors.Add(key, new[] { errorMessage });
                    }

                }
                else
                {
                    var errorMessages = new string[errors.Count];
                    for (var i = 0; i < errors.Count; i++)
                    {
                        errorMessages[i] = GetErrorMessage(errors[i]);
                        if (errorMessages[i] == myerror)
                        {
                            errorMessages[i] = "Cannot deserialize";
                        }
                    }

                    Errors.Add(key, errorMessages);
                }
            }
        }
    }

    string GetErrorMessage(ModelError error)
    {
        return string.IsNullOrEmpty(error.ErrorMessage) ?
            "The input was not valid." :
        error.ErrorMessage;
    }
}

Configure in your Startup.cs:

services.AddControllers().ConfigureApiBehaviorOptions(options =>
{
    options.InvalidModelStateResponseFactory = context =>
    {
        var problems = new CustomBadRequest(context);

        return new BadRequestObjectResult(problems);
    };
});

Be sure your Controller contains [ApiController] attribute.

Result:

enter image description here