Enums in API design: Everything you need to know

Are enums a clever validation technique or dangerous design decision? Well, they can be both, depending on how and why you use them. As such, using enums in API design requires careful thought.

This is a multi-part series on API design guidance, where we take a look at tricks and hidden troubles in API designs and how to avoid them. While these articles may not be exhaustive, they will serve to identify common patterns and anti-patterns in API design. The full series includes:

What is enum in API design?

Let’s start with a quick enum description. Enums (short for enumeration), or enumerated types, are variable types that have a limited set of possible values. This can be both a blessing and a curse. 

Enums are popular in API design, as they are often seen as a simple way to communicate a limited set of possible values for a given property. However, enums come with some challenges that may limit or impede your API design. Below, we’ll look at the dangers of using enums within your API design and techniques for avoiding them.

Best practices for enums in API design

When designing APIs that make use of enums, certain practices will depend on whether it’s a Java API enum, a JSON enum, an OpenAPI enum and so on. That said, there are several overarching best practices to bear in mind, namely:

  • Minor changes may have a big impact – remember that even minor-seeming changes when adding or removing enum values in responses can break API client code.
  • Watch consistency of values – if your list of values is unknown or might change over time, avoid enums.
  • Include enums’ descriptions in their properties – this will reduce API client effort, as well as providing a way to obtain the complete list of values for open-ended or dynamic enums.
  • Avoid using enums to indicate client behaviour – look to hypermedia links instead in this scenario.
  • Avoid overusing enums – while enums are useful, they aren’t always the right choice. Overusing enums can make the API rigid and harder to evolve.
  • Provide clear error messages – when an API client uses an invalid enum value, return a clear and informative error message to help them correct the mistake.
  • Use enums to enforce type safety – enums can help enforce type safety in your API management, reducing the risk of invalid values being used.

We explore these elements in depth below.

Use enums for a fixed set of values

Broadly speaking, if you’re working with a fixed set of values that will never change, it’s time for enums. In this case, enums constitute a solid API design decision.You can implement this and your other design decisions quickly and easily with Tyk. 

There is also a case for using enums for input where they can be more resistant in the face of a change to the list of valid values. Read on for more on both use cases, along with key design points to bear in mind.

Get started with Tyk for free

How API clients handle enums is unpredictable

Let’s assume we have a simple Tasks API with a ListTasks operation that returns the description and status for our tasks. The status property is an enum that contains one of: activecomplete, and archived. Here is what our operation might return:

GET /tasks

[
  { "description": "Task 1", "status": "active" },
  { "description": "Task 2", "status": "complete" },
  { "description": "Task 3", "status": "archived" }
]

Seems simple and useful, yet there is a design flaw that lurks within. What if our status property is extended to support a new value: overdue. The code that consumes this API currently knows how to display tasks with an activecomplete, and archived status – but it doesn’t know anything about an overdue status. API providers may assume that the API client code can deal with this, but that isn’t always the case. There may be a switch statement based on the status enum, as in the code example below to conditionally show or hide buttons:

switch( task.getStatus() ) {
  case "active":
    // display the complete and archive buttons
    break;
  case "complete":
    // display archive button
    break;
  case "archived":
    // display re-activate button
    break;
  default:
    // what happens if we have a new status value???
}

When using enums, it is important for the API designer to understand that the client may choose to do whatever they like with those enums. They may use them to display a value and therefore may be more resilient to change. Or, they may use them to offer buttons for a user to take action, preventing newly added enum values from being recognised and acted upon properly. It is even possible that the client code is automatically generated from an OpenAPI Specification (OAS) document and the generator will not generate evolvable client code to automatically handle new values. As such, this is an issue that any developer making OpenAPI enum design decisions needs to be conscious of.

The same error may not occur if the enum is used as an input value in an API request. Let’s look at the example of an UpdateTask operation that allows the API client to change the status of a task:

PUT /tasks/1

{ "description": "Task 1", "status": "complete" }

In this example, the client knows of the three valid enum values: activecomplete, and archived. If the API provider then adds overdue as a new enum value, the API client will continue to call the UpdateTask operation successfully – without knowing about the new enum value – using the three previously known enum values. However, when the code calls the ListTasks operation and encounters a task whose status is overdue, the code may fail to process the response correctly.

Enum Design Tip 1: Assume that adding or removing enum values in API responses will break API client code – even if the change seems minimal.

Using enums in an evolving API design

During the early stages of the API design process, resource properties may be a known set of values, such as the status property in the example above. As more details emerge during the design process, an enum property may be identified as more dynamic than first assumed. This case is common for properties that are user-defined or perhaps sourced from a dynamic set of values or from a database lookup table.

When designing an API that uses enums, consider the value list as frozen. Assume that the values will never change, since we cannot control how the client will behave if an enum’s value list is expanded with new values or if values are removed.

When a property’s list of values is unknown or may change over time, start by using a string type for the property. Over time, if the values do not change, then the field may be changed to an enum.

Remember: It is easier to turn a string into an enum if it is determined that values do not change rather than to force client code to adapt to newly added values that emerge in the future. This means it’s important to avoid overusing enums from the outset of your design. Doing so can make your API harder to evolve, as it will be too rigid to flex and adapt as your requirements change.

Enum Design Tip 2: Avoid enums when the list of values is unknown or may change over time.

Adding enum descriptions to speed integration

While enums are often easy to convert into display strings, some may not be as theyMayBeCamelCaseAndRequireSomeCleanupForEndUsers. When this happens, client code is often forced to use the switch statement once again to handle enums:

switch( task.getStatus() ) {
  case "active":
    displayName = "Active";
    break;
  case "complete":
    displayName = "Complete";
    break;
  case "archived":
    displayName = "Archived";
    break;
}

Every client integration will require conversion code like this. Instead, consider adding a property to the response that provides a default description:

GET /tasks

[
  { "description": "Task 1", "status": "active", "statusDescription": "Active" },
  { "description": "Task 2", "status": "complete", "statusDescription": "Complete" },
  { "description": "Task 3", "status": "archived", "statusDescription": "Archived" }
]

If an application wishes to change the descriptions displayed, they are still able to do so. However, the API provides the description to speed integration. You may choose to add support for HTTP language negotiation to offer localised versions of the descriptions, if desired.

On the subject of reducing client effort, it’s also important to provide clear error messages, so that an API client can understand precisely what is happening when an invalid enum value is used. The more clear and informative the error message, the easier it will be to correct the mistake. Bear in mind, too, that you can use enums to reduce the risk of invalid values being used, using them to enforce type safety. 

Enum Design Tip 3: Enum properties should also include descriptions to reduce API client effort while remaining evolvable.

Supporting dynamic or open-ended enums through value lists

As mentioned earlier, sometimes properties change often enough that they should be typed as an open string rather than an enum. For properties that have a dynamic list of values, offer an operation for apps to obtain and display the list. The list may be exhaustive, or filtered based on existing state or user-based input. For example, we may offer a dynamic list of possible tags for our tasks:

GET /task-tags

[
  { "tagId": "my-tag", "description": "My Tag" },
  { "tagId": "my-other-tag", "description": "My Other Tag" }
]

In this example, the tags may be system-managed and only change occasionally, or they may be user-defined and allow for the addition or removal of tags over time. Notice that a description is supplied along with the id of the tag to ensure that applications are able to display more than just the identifier, offering a better user experience.

Enum Design Tip 4: Open-ended or dynamic enums must offer a way to obtain the complete list of values and must include descriptions.

Drive client behaviour through hypermedia, not enums

Earlier, we discussed the use of a switch statement to determine which buttons should be rendered in an app:

switch( task.getStatus() ) {
  case "active":
    // display the complete and archive buttons
    break;
  case "complete":
    // display archive button
    break;
  case "archived":
    // display re-activate button
    break;
  // ... more cases go here...
}

But, what if an app decided to show all buttons rather than build this conditional display logic? The user might have a poor user experience if they attempted to click the “complete” button for a task that has a status of archived.

While allowing clients to determine what actions a user has available based on an enum is a common approach to API design, this approach has its disadvantages. The biggest disadvantage is that every app must re-implement the same behaviour, resulting in business logic being spread across apps. If you have ever had a different user experience between a web and mobile app, you have witnessed this problem first-hand.

Hypermedia links may be used to convey server-side state and possible behaviours (i.e. affordances) to the client, enabling apps to become more dynamic and resilient to API changes. Let’s look at our task API example once again, but include hypermedia links to indicate which buttons to display:

GET /tasks

[
  {
    "description": "Task 1",
    "status": "active",
    "statusDescription": "Active",
    "_links": {
      "rel": "complete", "href":"/tasks/1/complete",
      "rel": "archive", "href":"/tasks/1/archive",
    }
  },
  {
    "description": "Task 2",
    "status": "complete",
    "statusDescription": "Complete",
    "_links": {
      "rel": "archive", "href":"/tasks/1/archive"
    }
  },
  {
    "description": "Task 3",
    "status": "archived",
    "statusDescription": "Archived",
    "_links": {
      "rel": "reactivate", "href":"/tasks/1/reactivate"
    }
  },
]

Our application can look for the presence or absence of these links and adjust the user interface accordingly. If additional rules are added later to limit archiving to project managers, the API can limit the links offered based on both the status and the role of the user – without any app modifications required! This is a powerful way to signal clients on what they can and cannot do based on any number of factors, including the use of role-based access control (RBAC).

Enum Design Tip 5: Don’t use enums to indicate client behaviour – use hypermedia links instead.

Enumerating your design options

Enums seem like a great idea, until you realise that they can create breaking changes when modified. In general, use enums only when they will never change, or when they are used for input and can be more resistant to a change in the list of valid values. For fields that change often, avoid enums in favour of a string combined with an operation to obtain the latest set of values with descriptions. Not only will this make your code more resilient to change, it will also make it dynamic while remaining user-friendly.

The bottom line

This user-friendliness should be a key consideration when it comes to your enum API decisions, as well as other elements of your API design. The API product landscape is becoming increasingly competitive, so focusing on the user experience of your products – whether in relation to when to use enum or anything else – has never been more important.

For information on API design, you can read more on: