Validating Enums in .Net WebAPI

Recently, I needed to implement some validation rules on a .Net WebAPI. The API in question accepted a payload that, amongst other things, included a currency code (e.g. “GBP”, “EUR”, “USD”). On the face of it, this sounds pretty simple but there are a few things to watch out for in order to do this well.

We might start with something like the following code

using System.Text.Json;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
    private readonly ILogger<ExampleController> _logger;

    public ExampleController(ILogger<ExampleController> logger)
    {
        _logger = logger;
    }

    [HttpPost("payments")]
    public IActionResult Payments(Payment payment)
    {
        _logger.LogInformation(JsonSerializer.Serialize(payment));
        return this.Ok();
    }
}

/// <summary>
/// This is the type we accept as the payload into our API.
/// </summary>
public class Payment
{
    public Currency Currency { get; set; }

    public decimal Amount { get; set; }
}

/// <summary>
/// This is an example enumeration.
/// We want our API consumers to use currencies as strings.
/// </summary>
public enum Currency
{
    GBP,
    EUR,
    USD
}

The first problem we have here is that the Currency value in our Json payload has to be a number. Out of the box, the API won’t accept a string such as “USD” and we’ll get a standard “400 Bad Request” validation failure response.

Accepting Enum String Values

We can easily fix this issue by using the JsonStringEnumConverter class that is part of System.Text.Json. To do this we decorate the enum declaration with the JsonConverter attribute. Optionally, we could have added the attribute to the individual property, but if we decorate the enum declaration, strings will be acceptable wherever it used.

/// <summary>
/// This is an example enumeration.
/// We want our API consumers to use currencies as strings.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Currency
{
    GBP,
    EUR,
    USD,
}

We can now pass in Currency as a string value. Great! If we pass an invalid string, we’ll get a 400 Bad Request. Even better!

Limiting to Valid Enum Values

What we have now is a situation where, if we provide a string value, the validation insists that it is a valid member of the Currency enum. However, because this is an enum, we can also pass in a number. This number does not get validated, so in our controller our payment object contains an invalid value. We can limit the field to valid values by adding an attribute to the property, like so:

    [EnumDataType(typeof(Currency))]
    public Currency Currency { get; set; }

Making Enum Mandatory

What if we want this to be a mandatory field that must be supplied by our caller? We might assume that this is easy – simply add the Required attribute to our Currency property. Just like this:

    [Required, EnumDataType(typeof(Currency))]
    public Currency Currency { get; set; }

Unfortunately, that doesn’t give us the behaviour we want. The field will be marked as mandatory in Swagger/OpenAPI if we are using that, but if we omit the field entirely from our payload, the POST is accepted.

Why would this be? The reason for this is that the underlying type for all enums is a numeric type. Unless specified otherwise, this will be int. That means that enums are value types and when initialised will default to 0. As far as the .Net validation process is concerned, the value is always present, so the Required attribute doesn’t have the effect we need.

To address this is also a simple modification, provided that you can treat 0 as an invalid value. Simply set the first enum value to a non-zero value:

/// <summary>
/// This is an example enumeration.
/// We want our API consumers to use currencies as strings.
/// We also set the first value to 1 so that 0 is invalid.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Currency
{
    GBP = 1,
    EUR,
    USD,
}

We now have an enum property that

  • can be specified as a string
  • is mandatory so must be present in the payload
  • must be a valid enum value
  • is documented appropriately in Swagger/OpenAPI