ASP.NET MVC How to convert ModelState errors to json
Asked Answered
B

15

137

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...

Belvabelvedere answered 16/5, 2010 at 23:9 Comment(0)
T
206

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()
);
Tamboura answered 16/5, 2010 at 23:11 Comment(6)
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 messageBelvabelvedere
How do you want to combine them?Tamboura
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?Belvabelvedere
Add .Where(kvp => kvp.Value.Errors.Count > 0)Tamboura
@Slacks in your Where you should use Any instead of Count>0 .Where(kvp => kvp.Value.Errors.Any())Shizukoshizuoka
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 ExceptionMessagesKristof
B
77

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());
    }
}
Belvabelvedere answered 17/5, 2010 at 3:25 Comment(4)
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.Damondamour
@Damondamour are you forgetting to reference System.Linq? ToDictionary() is a LINQ extension method.Forest
Subject to your preferences .Where(m => m.Value.Count() > 0) could also be written as .Where(m => m.Value.Any()).Titular
This can be used similarly as the ModelState.ToDataSourceResult() from Kendo.Mvc to return errors to the Grid and display error messages in editing.Crucifixion
K
23

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."
      ]
   }
}
Kenway answered 4/9, 2013 at 21:9 Comment(0)
A
11

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."
    ]
  }
}
Aloha answered 24/11, 2015 at 8:24 Comment(2)
BadRequest is WebAPI specific and this question is about MVC.Doggery
Just a note for new viewers: BadRequest is available all over now with .NET Core upwardsAloha
G
8

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));
}
Garter answered 29/11, 2013 at 12:11 Comment(1)
You can also return BadRequest(ModelState) and it will serialiaze it into JSON for you.Rattan
H
5

@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;
        }
Hereafter answered 25/11, 2012 at 4:47 Comment(0)
R
5

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

Reunionist answered 15/8, 2018 at 4:34 Comment(1)
How is this any different to this answer over 2+ years earlier?Schlessel
M
3

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

Mixer answered 14/10, 2015 at 9:1 Comment(0)
F
2

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;
        }

    }

}
Frodin answered 10/9, 2010 at 18:31 Comment(0)
D
2

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);
        }
Duma answered 20/9, 2013 at 12:12 Comment(0)
H
1

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;
    }
}
Hyoscyamine answered 31/1, 2013 at 16:41 Comment(0)
P
1

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."
        ]
    }
}
Pyotr answered 22/12, 2020 at 18:35 Comment(0)
B
0

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);
    }
Brotherinlaw answered 15/10, 2018 at 6:39 Comment(0)
T
0

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://learn.microsoft.com/en-us/answers/questions/620570/net-core-web-api-model-validation-error-response-t.html

Triglyceride answered 6/6, 2022 at 8:52 Comment(0)
S
-1
  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;
}
Sungsungari answered 6/2, 2017 at 16:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.