When returning a BadRequest(ModelState) or using automatic model validation, the json serialized response is PascalCased.
I.e.
{ Email: ["The Email field is not a valid e-mail address."] }
As discussed in the MVC project https://github.com/aspnet/Mvc/issues/5590 and well explained in this comment this is undesirable in SPA contexts. The javascript client would send requests like { email: "" } and should assume to get responses with the same key.
Using PascalCase here exposes C# naming convention, but should be an implementation detail.
Tested with ASP NET CORE 2.1
Thanks for contacting us, @jannikbuschke.
In 3.0 (still in preview) we've added an additional configuration option to control casing for dictionary keys.
Here is an example showing how to control it:
services.AddMvc()
.AddNewtonsoftJson(mvcNewtonsoftJsonOptions => mvcNewtonsoftJsonOptions.UseCamelCasing(processDictionaryKeys: true));
Since v3 is still in preview shouldn't this be addressed for 2.x and not closed?
Thanks for contacting us, @jannikbuschke.
In 3.0 (still in preview) we've added an additional configuration option to control casing for dictionary keys.
Here is an example showing how to control it:services.AddMvc() .AddNewtonsoftJson(mvcNewtonsoftJsonOptions => mvcNewtonsoftJsonOptions.UseCamelCasing(processDictionaryKeys: true));
I think this only works if you're serializing the ModelStateDictionary directly in your response. If you need to transform it into some other object that isn't a dictionary you're stuck with the original keys.
I would really like the key to be in the same format that the request had. Yes, we can ensure we handle this case-insensitively on the API side, but we cannot guarantee what a consumer of our service will do. If a client decides to send a field "likeThis" and we return a 400 with details about an error for a key "LikeThis", their client software may not be able to look up which field that maps to on their side.
Just discovered another flaw in the ProcessDictionaryKeys solution - it doesn't work for deep property paths. For example, if there is a validation issue with, say, a property on an item in a list, the ModelState key might be "List[0].MyProperty". Processing dictionary keys results in "list[0].MyProperty" but we want "list[0].myProperty" - or whatever the request used.
Seems like if you are using the System.Text.Json stack and its defaults are camelCase that it should trickle down to model validation errors for consistency. Shouldn't this be reopened to handle that? @rynowak
This is also a issue in my case with System.Text.Json stack. As a workaround, I have written a custom ProblemsDetailFactory.
Basically copied the DefaultProblemsDetailsFactory and modified a little bit to have the keys with the correct casing as specified in AddJsonOptions(options => options.JsonSerializerOptions.PropertyNamingPolicy = ...) so that error keys are consistent with whatever naming policy we configure.
// instead of new ValidationProblemDetails(modelStateDictionary) we compose the
// dicitionary with the correct casing per options
var errors = modelStateDictionary.ToDictionary(
kvp => jsonOptions?.JsonSerializerOptions?.PropertyNamingPolicy?.ConvertName(kvp.Key) ?? kvp.Key,
kvp => kvp.Value.Errors.Select(x => x.ErrorMessage).ToArray()
) ;
var problemDetails = new ValidationProblemDetails(errors)
{
...
};
Then we just need to register our custom ProblemDetailsFactory into mvc
// register our own problem details factory
services.AddSingleton<ProblemDetailsFactory, CustomProblemDetailsFactory>();
// add other mvc stuff
services
.AddControllers()
.AddJsonOptions(options =>
{
// tweak as needed
options.JsonSerializerOptions.PropertyNamingPolicy = new JsonSnakeCaseNamingPolicy();
})
;
Full CustomProblemDetailsFactory
using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Options;
namespace Spicy.ApiCommon.Mvc
{
public class CustomProblemDetailsFactory : ProblemDetailsFactory
{
private readonly ApiBehaviorOptions options;
private readonly JsonOptions jsonOptions;
public CustomProblemDetailsFactory(IOptions<ApiBehaviorOptions> options, IOptions<JsonOptions> jsonOptions)
{
this.options = options?.Value ?? throw new ArgumentNullException(nameof(options));
this.jsonOptions = jsonOptions?.Value ?? throw new ArgumentNullException(nameof(jsonOptions));
}
public override ProblemDetails CreateProblemDetails(
HttpContext httpContext,
int? statusCode = null,
string title = null,
string type = null,
string detail = null,
string instance = null)
{
statusCode ??= 500;
var problemDetails = new ProblemDetails
{
Status = statusCode,
Title = title,
Type = type,
Detail = detail,
Instance = instance,
};
ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value);
return problemDetails;
}
public override ValidationProblemDetails CreateValidationProblemDetails(
HttpContext httpContext,
ModelStateDictionary modelStateDictionary,
int? statusCode = null,
string title = null,
string type = null,
string detail = null,
string instance = null)
{
if (modelStateDictionary == null)
{
throw new ArgumentNullException(nameof(modelStateDictionary));
}
statusCode ??= 400;
var errors = modelStateDictionary.ToDictionary(
kvp => jsonOptions?.JsonSerializerOptions?.PropertyNamingPolicy?.ConvertName(kvp.Key) ?? kvp.Key,
kvp => kvp.Value.Errors.Select(x => x.ErrorMessage).ToArray()
) ;
var problemDetails = new ValidationProblemDetails(errors)
{
Status = statusCode,
Type = type,
Detail = detail,
Instance = instance,
};
if (title != null)
{
// For validation problem details, don't overwrite the default title with null.
problemDetails.Title = title;
}
ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value);
return problemDetails;
}
private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails, int statusCode)
{
problemDetails.Status ??= statusCode;
if (options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData))
{
problemDetails.Title ??= clientErrorData.Title;
problemDetails.Type ??= clientErrorData.Link;
}
var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier;
if (traceId != null)
{
problemDetails.Extensions["traceId"] = traceId;
}
}
}
}
Would be great if the mvc default implementation would handle the ModelState keys in this way!!
@pranavkm @mkArtakMSFT @Eilon This seems like it was closed early, like the issue wasn't actually addressed. Can someone confirm either:
?
Most helpful comment
Seems like if you are using the System.Text.Json stack and its defaults are camelCase that it should trickle down to model validation errors for consistency. Shouldn't this be reopened to handle that? @rynowak