'How to handle enum as string binding failure when enum value does not parse

In our ASP.net Core Web API application I am looking for a way to catch binding errors when my controller method accepts a complex object which has an ENUM property when ENUMs are de/serialized as strings.

eg.

class Person
{
    public string Name {get; set;}
    public SexEnum Sex {get; set;}
}

enum SexEnum
{
    Male,
    Female,
    Other
}

We use system wide StringEnumConverter so a JSON serialized instance of Person looks like so:

{
    "name": "Ann",
    "sex": "female"
}

Now if I post this JSON (note the typo in the sex property):

{
    "name": "Ann",
    "sex": "femal"
}

the whole object received by the controller method is NULL as binding failed.

I would like to catch that binding error and, instead of having the pipeline go into the controller as if nothing is wrong, return a BAD REQUEST to the client including the detail of which property value failed to bind.

I know the type I am trying to deserialize into, I know the property type I am trying to deserialize and I can see the value does not parse into the type. So I think there must be a way of providing that detail to the client. I just don't know where and how to plug this in.

I would like the solution to be system wide so that all enums are covered, without having to put attributes on the properties of the model or on the enums themselves. (This is because we distribute our API models as a nuget package which cannot have any dependencies.)



Solution 1:[1]

We had this issue recently and wrote our own attribute to handle it:

public class ValidEnumValueAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        Type enumType = value.GetType();
        bool valid = Enum.IsDefined(enumType, value);

        if(!valid)
        {
            return new ValidationResult($"{value} is not a valid value for type {enumType.Name}");
        }

        return ValidationResult.Success;
    }
}

class Person
{
    public string Name {get; set;}

    [ValidEnumValue]
    public SexEnum Sex {get; set;}
}

The error is then added to the ModelState so you can use ModelState.IsValid to check if the values are valid.

if(!ModelState.IsValid)
{
    return BadRequest(ModelState);
}

EDIT

If you don't want to use an attribute then you can derive a new converter from NewtonSoft StringEnumConverter and have that check the value is valid before reading the json e.g.

public class validEnumConverter : StringEnumConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if(!Enum.IsDefined(objectType, reader.Value))
        {
            throw new ArgumentException("Invalid enum value");
        }

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

This is added to the JsonOptions in your startup class:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().AddJsonOptions(options =>
    {
        options.SerializerSettings.Converters.Add(new validEnumConverter());
    });
}

Solution 2:[2]

Expanding on @Simply Ged's excellent answer, 2nd part, using a nullable enum produces System.ArgumentException: 'Type provided must be an Enum.' exception. An extra step is required to handle nullable enum, or a null value for the enum:

public class validEnumConverter : StringEnumConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        Type enumType = (Nullable.GetUnderlyingType(objectType) ?? objectType);
        if(!Enum.IsDefined(enumType, reader.Value ?? string.Empty))
        {
            throw new ArgumentException("Invalid enum value");
        }

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

Solution 3:[3]

Just got this issue recently. The way I overcome that was applying

[EnumDataType(typeof(YOUR_ENUM_TYPE))] above your model enum.

Example:

public class SaladModel
{
   [EnumDataType(typeof(SauceTypeEnum))]
   public SauceTypeEnum SauceType { get; set; }
}

Now once you post this to the WebAPI endpoint, its validated and returns as BadRequest by the framework.

Solution 4:[4]

You do this with the JsonStringEnumConverter converter.

public class PersonModel
{
    public string Name { get; set; }

    [JsonConverter(typeof(JsonStringEnumConverter))]
    public SexKind Sex { get; set; }
}

public enum SexKind
{
    Male,
    Female,
    Confused
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 gds03
Solution 4 Fred