11

Is it possible to return the [Required] attribute error message when a JSON request doesn't provide a proper value for an enum property?

For example, I have a model for a POST message that contains an AddressType property that is an enumeration type:

public class AddressPostViewModel
{
    [JsonProperty("addressType")]
    [Required(ErrorMessage = "Address type is required.")]
    public AddressType AddressType { get; set; }
}

The AddressType enum accepts two values:

[JsonConverter(typeof(StringEnumConverter))]
public enum AddressType
{
    [EnumMember(Value = "Dropship")]
    Dropship,
    [EnumMember(Value = "Shipping")]
    Shipping
}

I've noticed (or actually my QA team noticed) that if the request message JSON contains either an empty string or null for the AddressType, the error message isn't the expected Address type is required. message. Instead, the error message is a somewhat unfriendly parsing error.

For example, if the request JSON looks like this:

{  "addressType": "" }

Then the error that is auto-generated by the validation framework looks like this:

{
    "message": "Validation Failed",
    "errors": [
        {
            "property": "addressType",
            "message": "Error converting value \"\" to type 'MyNamespace.AddressType'. Path 'addressType', line 4, position 19."
        }
    ]
}

Is there a way to ensure that error message of the [Required] attribute is returned if someone doesn't include a valid value for an enum?

TylerH
  • 20,799
  • 66
  • 75
  • 101
PoorInRichfield
  • 1,436
  • 2
  • 19
  • 29
  • 1
    I'd say validation is working fine. An enum is a set of names on top of a base numeric type. They aren't nullable, so unless set, their value is the default value of their base type (ie 0). An empty string isn't a valid enum name. `null` isn't valid either, precisely because an enum isn't nullable. – Panagiotis Kanavos Jan 15 '19 at 16:31
  • If you want to "accept" missing values, the type would have to be nullable, ie `AddressType?`. – Panagiotis Kanavos Jan 15 '19 at 16:33
  • @PanagiotisKanavos I agree with you in that the validation is working correctly, it's just not what I want. I do not wish to accept nulls or empty strings, I just want to present a user-friendly message if the client sends either of those values. – PoorInRichfield Jan 15 '19 at 16:51
  • You could also use a validation package like FluentValidation for this specific case or all your validations. – PmanAce Jan 15 '19 at 18:18

6 Answers6

10

Option 1: To use Custom RequiredEnum attribute and avoid JsonConverter attribute

Do not put JsonConverter on the AddressType enum. This StringToEnum is failing to map the string.Empty to enum value and it is throwing this error message.

Instead of that, you can write a custom required enum validator as shown below.

    using System;
    using System.ComponentModel.DataAnnotations;

    public class RequiredEnumFieldAttribute: RequiredAttribute
    {
        public override bool IsValid(object value)
        {
            if (value == null)
            {
                 return false;
            }

            var type = value.GetType();
            return type.IsEnum && Enum.IsDefined(type, value);
        }
   }

Then you can use it like shown below:

public class AddressPostViewModel
{
    [JsonProperty("addressType")]
    [RequiredEnumField(ErrorMessage = "Address type is required.")]
    public AddressType AddressType { get; set; }
}

Option 2: Use custom JsonConverter for AddressType

Add one custom CustomStringToEnumConverter which is derived from StringEnumConverter.

This method would throw an error if the value in JSON is empty.

public class CustomStringToEnumConverter : StringEnumConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (string.IsNullOrEmpty(reader.Value.ToString()))
            throw new Exception("Address not provided");

        return base.ReadJson(reader, objectType, existingValue, serializer);
    }
}

Use this jsonConverter instead of default StringEnumConverter as shown below

[JsonConverter(typeof(CustomStringToEnumConverter))]
public enum AddressType
{
}
Manoj Choudhari
  • 5,277
  • 2
  • 26
  • 37
  • The custom required enum attribute code is only hit if I don't include the AddressType property in the JSON request at all. If I set AddressType to an empty string or null, I get the same undesired "Error converting value" error. – PoorInRichfield Jan 15 '19 at 17:20
  • +1, but if you throw the exception in the ReadJson method, the message in the required attribute still won't be displayed. OP should look at https://stackoverflow.com/questions/20104575/how-to-handle-deserialization-of-empty-string-into-enum-in-json-net – Mikołaj Mularczyk Jan 15 '19 at 18:18
  • @MikołajMularczyk - From the throw new exception we can throw the message which want to show on UI. Another option is as you stated, we can map NULL or EMPTY string to some Enum value and then use custom attribute to throw error message. But for that we will have to add a new value to Enum which will map to undefined. – Manoj Choudhari Jan 15 '19 at 18:32
  • You can have any message you want inside the exception, that is true, but unless OP has some middleware that catches and handles the exception, the consumer will get the internal server error (500). – Mikołaj Mularczyk Jan 15 '19 at 18:53
  • Unfortunately, Option 1 does not work even when removing the StringEnumConverter attribute from the AddressType. I still get the "Error converting value" exception. Option 2 is looking a bit more promising as the custom converter code is getting triggered prior to the "Error converting value" error. – PoorInRichfield Jan 15 '19 at 19:26
5

I've come-up with a solution that meets my requirements, although the code makes me cringe a little.

I kept the [Required] attribute on the AddressType property in the view model. The cringe-worthy part is that I had to make the property nullable:

public class AddressPostViewModel
{
    [JsonProperty("addressType")]
    [Required(ErrorMessage = "Address type is required.")]
    public AddressType? AddressType { get; set; }
}

On the AttributeType enum itself, I replaced the StringEnumConverter attribute with a custom JsonConverter as suggested by @Manoj Choudhari:

[JsonConverter(typeof(CustomStringToEnumConverter))]
public enum AddressType
{
    [EnumMember(Value = "Dropship")]
    Dropship,
    [EnumMember(Value = "Shipping")]
    Shipping
}

This is the CustomStringToEnumConverter:

public class CustomStringToEnumConverter : StringEnumConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (string.IsNullOrEmpty(reader.Value?.ToString()))
        {
            return null;
        }

        object parsedEnumValue;

        var isValidEnumValue = Enum.TryParse(objectType.GenericTypeArguments[0], reader.Value.ToString(), true, out parsedEnumValue);

        if (isValidEnumValue)
        {
            return parsedEnumValue;
        }
        else
        {
            return null;
        }
    }
}

The CustomStringToEnumConverter can handle empty strings, nulls, and invalid strings. If it encounters an invalid enum value, it returns null which is then caught when the required field validation (magic) occurs and the RequiredAttribute error message is displayed in the JSON response.

While I don't like making the AttributeType type nullable, the consumer of my API will see a consistent validation message if the AttributeType value is wrong in the request JSON.

Ian Kemp
  • 28,293
  • 19
  • 112
  • 138
PoorInRichfield
  • 1,436
  • 2
  • 19
  • 29
  • Thank you! Took me a couple of hours to analyze my problem, formulate a question and find your answer ;-) – citronas Oct 11 '19 at 10:05
  • Please reset the values of enum by setting first enum value to anything other than '0'. This will force system as default value '0' is not a valid enum – Navap Jan 06 '20 at 13:28
  • I got an error in this line var isValidEnumValue = Enum.TryParse(objectType.GenericTypeArguments[0], reader.Value.ToString(), true, out parsedEnumValue); TryParse doesn't expect 4 params – kerzek Nov 20 '20 at 18:41
3

I don't think there is an out of the box data annotation validating enum values.

You can derive from the required attribute though:

using System;
using System.ComponentModel.DataAnnotations;

    public class RequiredEnumAttribute : RequiredAttribute
    {
        public override bool IsValid(object value)
        {
            if (value == null) return false;
            var type = value.GetType();
            return type.IsEnum && Enum.IsDefined(type, value);
        }
}

The Enum.IsDefined method is checking if a given value exists in the enum of given type.

Usage:

[RequiredEnum(ErrorMessage = "Your error message.")]
public YourEnum EnumProperty { get; set; }

See this article.

Mikołaj Mularczyk
  • 959
  • 12
  • 20
  • The custom required enum attribute code is only hit if I don't include the AddressType property in the JSON request at all. If I set AddressType to an empty string or null, I get the same undesired "Error converting value" error. – PoorInRichfield Jan 15 '19 at 17:20
  • Then your problem is with the JsonConverter, it is not able to parse an empty string. As @Manoj Chudari pointed out, you have to implement a custom one. You should look at this thread https://stackoverflow.com/questions/20104575/how-to-handle-deserialization-of-empty-string-into-enum-in-json-net – Mikołaj Mularczyk Jan 15 '19 at 18:13
3

(see the full write-up here: https://mtarleton.medium.com/enum-as-required-field-in-asp-net-core-webapi-a79b697ef270)

The problem faced here is the order of operations in converting the body to JSON and validating the model validations.

First, .NET de-serializes the body into JSON with default values. This means if you pass in a null value for a non-nullable ENUM the default value will be set for it (the first item in the ENUM usually). Same thing happens for primatives.

[JsonProperty(Required = Required.Always)]
Michael
  • 308
  • 3
  • 12
  • I tried the suggestion in your article, but it doesn't work. If I omit the property from the JSON, I get no error. If I put an invalid enum value, I get an error from the JSON deserializer rather than the error message from the ValidEnumAttribute – cdimitroulas Sep 13 '21 at 10:37
2

As pointed by this answer and docs, .NET 7 introduced [JsonRequired] attribute which can validate whether the enum value was present in the JSON. It doesn't return the exact same error as [Required] attribute but the validation works none the less.

Dilpreet
  • 37
  • 1
  • 6
0

Sample with overriding ValidationResult

 protected override ValidationResult? IsValid(object? value, ValidationContext? validationContext)
{
    var displayName = validationContext?.DisplayName ?? "";

    if (value == null)
    {
        return new ValidationResult($"{displayName} is required", new[] {displayName});
    }

    var type = value.GetType();
    if (type.IsEnum && Enum.IsDefined(type, value))
    {
        return ValidationResult.Success;
    }

    return new ValidationResult($"Invalid value supplied for {displayName}", new[] {displayName});
}
Sonya
  • 76
  • 1
  • 8