API design guidance: enums

Enums, or enumerated types, are variable types that have a limited set of possible values. 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. Let’s look at the dangers of using enums within your API design and techniques for avoiding them.

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.

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 provides 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.

The same error may not occur if the enum is used as an input value in an API request. Let’s example 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.

Enum Design Tip 2: Avoid enums when the list of values are 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.

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 behavior 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 behavior, 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 behaviors (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 behavior – use hypermedia links instead.

Enumerating your design options

Enums seem like a great idea, until you realize 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 favor 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.

For information on API design, you can read more on How to create an API style guide, and why.  and How to conduct an API design review.

© Tyk Technologies, 2020