12

We have a couple of models that override the name via JsonProperty, but this causes an issue when we get validation errors through ModelState. For example:

class MyModel
{
    [JsonProperty("id")]
    [Required]
    public string MyModelId {get;set;}
}

class MyModelController
{
    public IHttpActionResult Post([FromBody] MyModel model)
    {
        if (!ModelState.IsValid)
        {
            return HttpBadRequest(ModelState);
        }

        /* etc... */
    }
}

The above Post will return the error The MyModelId field is required. which isn't accurate. We'd like this to say The id field is required.. We've attempted using [DataMember(Name="id")] but get the same result.

Question 1: Is there a way we can get ModelState errors to show the JSON property name rather than the C# property name aside from providing our own error messages on every [Required] attribute?

-- Update --

I've been playing around with this and found a "do-it-yourself" method for re-creating the error messages using custom property names. I'm really hoping there's a built-in way to do this, but this seems to do the job...

https://gist.github.com/Blackbaud-JasonTremper/b64dc6ddb460afa1698daa6d075857e4

Question 2: Can ModelState.Key be assumed to match the <parameterName>.<reflectedProperty> syntax or are there cases where this might not be true?

Question 3: Is there an easier way to determine what the JSON parameter name is expected to be rather than searching via reflection on [DataMember] or [JsonProperty] attributes?

jt000
  • 3,196
  • 1
  • 18
  • 36

4 Answers4

3

Did you try using DisplayName attribute?

displayname attribute vs display attribute

Also, you can assign an error message to [Required] attribute.

[Required(ErrorMessage = "Name is required")]

Community
  • 1
  • 1
bobek
  • 8,003
  • 8
  • 39
  • 75
  • I did try [DisplayName] and it didn't work. However according to you're link the [Display] property did the job (maybe that's what you meant?). Thanks! – jt000 Nov 17 '16 at 14:21
3

I also faced this problem, I modified some code from your link to fit my WebAPI. modelState will also store the old key which is the variable name of the model, plus the Json Property names.

  1. First, create the filter ValidateModelStateFilter
  2. Add [ValidateModelStateFilter] above controller method

The filter source code:

public class ValidateModelStateFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var descriptor = actionContext.ActionDescriptor;
        var modelState = actionContext.ModelState;

        if (descriptor != null)
        {
            var parameters = descriptor.GetParameters();

            var subParameterIssues = modelState.Keys
                                               .Where(s => s.Contains("."))
                                               .Where(s => modelState[s].Errors.Any())
                                               .GroupBy(s => s.Substring(0, s.IndexOf('.')))
                                               .ToDictionary(g => g.Key, g => g.ToArray());

            foreach (var parameter in parameters)
            {
                var argument = actionContext.ActionArguments[parameter.ParameterName];

                if (subParameterIssues.ContainsKey(parameter.ParameterName))
                {
                    var subProperties = subParameterIssues[parameter.ParameterName];
                    foreach (var subProperty in subProperties)
                    {
                        var propName = subProperty.Substring(subProperty.IndexOf('.') + 1);
                        var property = parameter.ParameterType.GetProperty(propName);
                        var validationAttributes = property.GetCustomAttributes(typeof(ValidationAttribute), true);

                        var value = property.GetValue(argument);

                        modelState[subProperty].Errors.Clear();
                        foreach (var validationAttribute in validationAttributes)
                        {
                            var attr = (ValidationAttribute)validationAttribute;
                            if (!attr.IsValid(value))
                            {
                                var parameterName = GetParameterName(property);
                                // modelState.AddModelError(subProperty, attr.FormatErrorMessage(parameterName));
                                modelState.AddModelError(parameterName, attr.FormatErrorMessage(parameterName));
                            }
                        }
                    }
                }


            }
        }

    }

    private string GetParameterName(PropertyInfo property)
    {
        var dataMemberAttribute = property.GetCustomAttributes<DataMemberAttribute>().FirstOrDefault();
        if (dataMemberAttribute?.Name != null)
        {
            return dataMemberAttribute.Name;
        }

        var jsonProperty = property.GetCustomAttributes<JsonPropertyAttribute>().FirstOrDefault();
        if (jsonProperty?.PropertyName != null)
        {
            return jsonProperty.PropertyName;
        }

        return property.Name;
    }
}
code4j
  • 4,208
  • 5
  • 34
  • 51
0

You can access the parameter type, get the json name, and then replace the property name with the json name. Something like this:

var invalidParameters = (from m in actionContext.ModelState
                         where m.Value.Errors.Count > 0
                         select new InvalidParameter
                         {
                              ParameterName = m.Key,
                              ConstraintViolations = (from msg in m.Value.Errors select msg.ErrorMessage).ToArray()
                         }).AsEnumerable().ToArray();

if (actionContext.ActionDescriptor.Parameters.Count == 1)
{
    var nameMapper = new Dictionary<string, string>();

    foreach (var property in actionContext.ActionDescriptor.Parameters[0].ParameterType.GetProperties())
    {
        object[] attributes = property.GetCustomAttributes(typeof(JsonPropertyNameAttribute), false);

        if (attributes.Length != 1) continue;

        nameMapper.Add(property.Name, ((JsonPropertyNameAttribute) attributes[0]).Name);
    }

    var modifiedInvalidParameters = new List<InvalidParameter>();


    foreach (var invalidParameter in invalidParameters)
    {
        if(invalidParameter.ParameterName != null && nameMapper.TryGetValue(invalidParameter.ParameterName, out var mappedName))
        {
            var modifiedConstraintViolations = new List<string>();

            foreach (var constraintViolation in invalidParameter.ConstraintViolations ?? Enumerable.Empty<string>())
            {
                modifiedConstraintViolations.Add(constraintViolation.Replace(invalidParameter.ParameterName, mappedName));
            }

            modifiedInvalidParameters.Add(new InvalidParameter
            {
                ParameterName = mappedName,
                ConstraintViolations = modifiedConstraintViolations.ToArray()
            });
        }
        else
        {
            modifiedInvalidParameters.Add(invalidParameter);
        }
    }

    invalidParameters = modifiedInvalidParameters.ToArray();
}
public struct InvalidParameter
{
    [JsonPropertyName("parameter_name")]
    public string? ParameterName { get; set; }

    [JsonPropertyName("constraint_violations")]
    public string[]? ConstraintViolations { get; set; }
}
Eng.Fouad
  • 115,165
  • 71
  • 313
  • 417
0

For anyone stumbling across the same issue in .NET 6/7

This was recently fixed in .NET 7:

In order to use JsonPropertyName in System.Text.Json.Serialization, add the following to your configuration in Program.cs

builder.Services.AddControllers(options => 
            options.ModelMetadataDetailsProviders.Add(new SystemTextJsonValidationMetadataProvider()));

In order to use JsonProperty.Name in Newtonsoft.Json, add the following to your configuration in Program.cs

builder.Services.AddControllers(options => 
            options.ModelMetadataDetailsProviders.Add(new NewtonsoftJsonValidationMetadataProvider()));

.NET 7 Documentation

.NET 7 Issue

MaxKaluza
  • 1
  • 2