ChatGPT解决这个技术问题 Extra ChatGPT

ASP.NET MVC How to convert ModelState errors to json

How do you get a list of all ModelState error messages? I found this code to get all the keys: ( Returning a list of keys with ModelState errors)

var errorKeys = (from item in ModelState
        where item.Value.Errors.Any() 
        select item.Key).ToList();

But how would I get the error messages as a IList or IQueryable?

I could go:

foreach (var key in errorKeys)
{
    string msg = ModelState[error].Errors[0].ErrorMessage;
    errorList.Add(msg);
}

But thats doing it manually - surely there is a way to do it using LINQ? The .ErrorMessage property is so far down the chain that I don't know how to write the LINQ...


S
SLaks

You can put anything you want to inside the select clause:

var errorList = (from item in ModelState
        where item.Value.Errors.Any() 
        select item.Value.Errors[0].ErrorMessage).ToList();

EDIT: You can extract multiple errors into separate list items by adding a from clause, like this:

var errorList = (from item in ModelState.Values
        from error in item.Errors
        select error.ErrorMessage).ToList();

Or:

var errorList = ModelState.Values.SelectMany(m => m.Errors)
                                 .Select(e => e.ErrorMessage)
                                 .ToList();

2nd EDIT: You're looking for a Dictionary<string, string[]>:

var errorList = ModelState.ToDictionary(
    kvp => kvp.Key,
    kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray()
);

Thats a quick reply :)! Hey that looks good, but what if ModelState[item.Key] has more than 1 error? Errors[0] only works for a single error message
How do you want to combine them?
Thanks thats almost it - but its selecting every key even if it has no errors - how can we filter out the keys with no errors?
Add .Where(kvp => kvp.Value.Errors.Count > 0)
To get the same output like from Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); you should use var errorList = modelState.Where(elem => elem.Value.Errors.Any()) .ToDictionary( kvp => kvp.Key, kvp => kvp.Value.Errors.Select(e => string.IsNullOrEmpty(e.ErrorMessage) ? e.Exception.Message : e.ErrorMessage).ToArray()); Otherwise you won't have the ExceptionMessages
K
Kaushik Thanki

Here is the full implementation with all the pieces put together:

First create an extension method:

public static class ModelStateHelper
{
    public static IEnumerable Errors(this ModelStateDictionary modelState)
    {
        if (!modelState.IsValid)
        {
            return modelState.ToDictionary(kvp => kvp.Key,
                kvp => kvp.Value.Errors
                                .Select(e => e.ErrorMessage).ToArray())
                                .Where(m => m.Value.Any());
        }
        return null;
    }
}

Then call that extension method and return the errors from the controller action (if any) as json:

if (!ModelState.IsValid)
{
    return Json(new { Errors = ModelState.Errors() }, JsonRequestBehavior.AllowGet);
}

And then finally, show those errors on the clientside (in jquery.validation style, but can be easily changed to any other style)

function DisplayErrors(errors) {
    for (var i = 0; i < errors.length; i++) {
        $("<label for='" + errors[i].Key + "' class='error'></label>")
        .html(errors[i].Value[0]).appendTo($("input#" + errors[i].Key).parent());
    }
}

This looks like an interesting method however the helper class isn't working for me. Is this due to changes perhaps with MVC 2? I'm getting an error that the ToDictionary method doesn't exist on modelState.
@Cymen are you forgetting to reference System.Linq? ToDictionary() is a LINQ extension method.
Subject to your preferences .Where(m => m.Value.Count() > 0) could also be written as .Where(m => m.Value.Any()).
This can be used similarly as the ModelState.ToDataSourceResult() from Kendo.Mvc to return errors to the Grid and display error messages in editing.
J
Jovica Zaric

I like to use Hashtable here, so that I get JSON object with properties as keys and errors as value in form of string array.

var errors = new Hashtable();
foreach (var pair in ModelState)
{
    if (pair.Value.Errors.Count > 0)
    {
        errors[pair.Key] = pair.Value.Errors.Select(error => error.ErrorMessage).ToList();
    }
}
return Json(new { success = false, errors });

This way you get following response:

{
   "success":false,
   "errors":{
      "Phone":[
         "The Phone field is required."
      ]
   }
}

D
Dean North

There are lots of different ways to do this that all work. Here is now I do it...

if (ModelState.IsValid)
{
    return Json("Success");
}
else
{
    return Json(ModelState.Values.SelectMany(x => x.Errors));
}

You can also return BadRequest(ModelState) and it will serialiaze it into JSON for you.
E
Erik A. Brandstadmoen

The easiest way to do this is to just return a BadRequest with the ModelState itself:

For example on a PUT:

[HttpPut]
public async Task<IHttpActionResult> UpdateAsync(Update update)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    // perform the update

    return StatusCode(HttpStatusCode.NoContent);
}

If we use data annotations on e.g. a mobile number, like this, in the Update class:

public class Update {
    [StringLength(22, MinimumLength = 8)]
    [RegularExpression(@"^\d{8}$|^00\d{6,20}$|^\+\d{6,20}$")]
    public string MobileNumber { get; set; }
}

This will return the following on an invalid request:

{
  "Message": "The request is invalid.",
  "ModelState": {
    "update.MobileNumber": [
      "The field MobileNumber must match the regular expression '^\\d{8}$|^00\\d{6,20}$|^\\+\\d{6,20}$'.",
      "The field MobileNumber must be a string with a minimum length of 8 and a maximum length of 22."
    ]
  }
}

BadRequest is WebAPI specific and this question is about MVC.
Just a note for new viewers: BadRequest is available all over now with .NET Core upwards
s
spottedmahn

Simple way achieve this by using built-in functionality

[HttpPost]
public IActionResult Post([FromBody]CreateDoctorInput createDoctorInput) {
    if (!ModelState.IsValid) {
        return BadRequest(ModelState);
    }

    //do something
}

JSON result will be


h
h45d6f7d4f6f

@JK it helped me a lot but why not:

 public class ErrorDetail {

        public string fieldName = "";
        public string[] messageList = null;
 }

        if (!modelState.IsValid)
        {
            var errorListAux = (from m in modelState 
                     where m.Value.Errors.Count() > 0 
                     select
                        new ErrorDetail
                        { 
                                fieldName = m.Key, 
                                errorList = (from msg in m.Value.Errors 
                                             select msg.ErrorMessage).ToArray() 
                        })
                     .AsEnumerable()
                     .ToDictionary(v => v.fieldName, v => v);
            return errorListAux;
        }

A
Andy

Take a look at System.Web.Http.Results.OkNegotiatedContentResult.

It converts whatever you throw into it to JSON.

So I did this

var errorList = ModelState.ToDictionary(kvp => kvp.Key.Replace("model.", ""), kvp => kvp.Value.Errors[0].ErrorMessage);

return Ok(errorList);

This resulted in:

{
  "Email":"The Email field is not a valid e-mail address."
}

I am yet to check what happens when there is more than one error for each field but the point is the OkNegoriatedContentResult is brilliant!

Got the linq/lambda idea from @SLaks


p
philrabin

ToDictionary is an Enumerable extension found in System.Linq packaged in the System.Web.Extensions dll http://msdn.microsoft.com/en-us/library/system.linq.enumerable.todictionary.aspx. Here's what the complete class looks like for me.

using System.Collections;
using System.Web.Mvc;
using System.Linq;

namespace MyNamespace
{
    public static class ModelStateExtensions
    {
        public static IEnumerable Errors(this ModelStateDictionary modelState)
        {
            if (!modelState.IsValid)
            {
                return modelState.ToDictionary(kvp => kvp.Key,
                    kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray()).Where(m => m.Value.Count() > 0);
            }
            return null;
        }

    }

}

J
Jason Aller

Why not return the original ModelState object to the client, and then use jQuery to read the values. To me it looks much simpler, and uses the common data structure (.net's ModelState)

to return the ModelState as Json, simply pass it to Json class constructor (works with ANY object)

C#:

return Json(ModelState);

js:

        var message = "";
        if (e.response.length > 0) {
            $.each(e.response, function(i, fieldItem) {
                $.each(fieldItem.Value.Errors, function(j, errItem) {
                    message += errItem.ErrorMessage;
                });
                message += "\n";
            });
            alert(message);
        }

J
Jeff Circeo

Variation with return type instead of returning IEnumerable

public static class ModelStateHelper
{
    public static IEnumerable<KeyValuePair<string, string[]>> Errors(this ModelStateDictionary modelState)
    {
        if (!modelState.IsValid)
        {
            return modelState
                .ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray())
                .Where(m => m.Value.Any());
        }

        return null;
    }
}

D
David Peters

I ran into the same hurdle, wanting to control my 400 Bad Request output format but not wanting to get my hands dirty serializing the guts of ModelState. I would up using the sealed (but public, thankfully) SerializableError class.

var errorDetails = new SerializableError(ModelState);

var errorResponse = new YourCustomResponseType
{
    ModelValidationErrors = errorDetails,
    LogMessages = new []
    {
        new LogMessage("Error", "Invalid model - see modelValidationErrors for detail")
    }
};
return BadRequest(errorResponse);

Where YourCustomResponseType might look like this:

public class YourCustomResponseType
{
        public LogMessage[] LogMessages { get; set; }
        public Dictionary<string, object> ModelValidationErrors { get; set; }
}

SerializableError is a Dictionary<string, object> so this works out nicely. Your response might look like this:

{
    "logMessages": [
        {
            "category": "Error",
            "message": "Invalid model - see modelValidationErrors for detail"
        }
    ],
    "modelValidationErrors": {
        "aSettingsType.someEnumField": [
            "The input was not valid."
        ]
    }
}

N
Niyaz Mukhamedya

I made and extension that returns string with seperator " " (you can use your own):

   public static string GetFullErrorMessage(this ModelStateDictionary modelState) {
        var messages = new List<string>();

        foreach (var entry in modelState) {
            foreach (var error in entry.Value.Errors)
                messages.Add(error.ErrorMessage);
        }

        return String.Join(" ", messages);
    }

p
ping

You can use middleware for this:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers().ConfigureApiBehaviorOptions(options =>
{
    options.InvalidModelStateResponseFactory = context =>
    {
        var result = new ValidationFailedResult(context.ModelState);
        result.ContentTypes.Add(MediaTypeNames.Application.Json);
        return result;
    };
});

and ValidationFailedResult:

public class ValidationFailedResult : ObjectResult
    {
        public ValidationFailedResult(ModelStateDictionary modelState)
            : base(new ErrorResponse(modelState.Keys
                     .SelectMany(key => modelState[key].Errors.Select(x => new ApplicationError(StatusCodes.Status422UnprocessableEntity, key, x.ErrorMessage)))
                     .ToList()))
        {
            StatusCode = StatusCodes.Status422UnprocessableEntity; //change the http status code to 422.
        }
    }

and ApplicationError:

 public class ApplicationError
    {
        public ApplicationError(int code, string title, string detail)
        {
            Code=code;
            Title=title;
            Detail=detail;
        }

        public int Code { get; set; }
        public string Title { get; set; }
        public string Detail { get; set; }

        public override string ToString()
        {
            return $"Status:{Code} Title:{Title} Detail:{Detail}";
        }
    }

The result looks like this:

{
    "errors": [
        {
            "code": 422,
            "title": "$",
            "detail": "'\"' is invalid after a value. Expected either ',', '}', or ']'. Path: $ | LineNumber: 7 | BytePositionInLine: 1."
        },
        {
            "code": 422,
            "title": "input",
            "detail": "The input field is required."
        }
    ]
}

Source: https://docs.microsoft.com/en-us/answers/questions/620570/net-core-web-api-model-validation-error-response-t.html


C
CyberNinja
  List<ErrorList> Errors = new List<ErrorList>(); 


        //test errors.
        var modelStateErrors = this.ModelState.Keys.SelectMany(key => this.ModelState[key].Errors);

        foreach (var x in modelStateErrors)
        {
            var errorInfo = new ErrorList()
            {
                ErrorMessage = x.ErrorMessage
            };
            Errors.Add(errorInfo);

        }

if you use jsonresult then return

return Json(Errors);

or you can simply return the modelStateErrors, I havent tried it. What I did is assign the Errors collection to my ViewModel and then loop it..In this case I can return my Errors via json. I have a class/model, I wanted to get the source/key but I'm still trying to figure it out.

    public class ErrorList
{
    public string ErrorMessage;
}