Background image

OpenAPI Tips

OneOf, AllOf, AnyOf Oh my! How to define union types in OpenAPI

Nolan Sullivan

Nolan Sullivan

October 19, 2023

Featured blog post image
Success Icon

Announcing: OpenAPI Reference

Hi! These blog posts have been popular, so we've built an entire OpenAPI Reference Guide to answer any question you have.

It includes detailed information on polymorphic types.

Happy Spec Writing!

The OpenAPI Specification (OAS) is designed to be capable of describing any HTTP API, whether that be REST or something more akin to RPC-based calls. That leads to the OAS having a lot of flexibility baked-in: there are a lot of ways to achieve the exact same result that are equally valid in the eyes of the OAS.

That’s why we’re taking the time to eliminate some of the most common ambiguities that you’ll encounter when you build your OpenAPI documents (OADs). In this case, we’ll be taking a look at how to effectively use anyOf, allOf, and oneOf in your OpenAPI 3.X OADs.

The anyOf, allOf, and oneOf keywords are defined by JSON Schema and used in OpenAPI to define the structure and validation rules for data. They can be used together to define complex and flexible schemas.

  • oneOf: The value must match exactly one of the subschemas. The oneOf keyword is useful for describing scenarios where a property can be defined with multiple possible data structures, but only one of them is used at a time. For example, if your API accepts a string or an int for a certain field depending on the use case, oneOf would be used. In code generation, it will be generally interpreted as a union type.
  • allOf: The value must match all of the subschemas. The allOf keyword is useful for describing model composition: the creation of complex schemas via the composition of simpler schemas.
  • anyOf: The value must match one or more of the subschemas. The anyOf keyword is useful for describing type validation (similar to oneOf), but it can get you into a lot of trouble in code generation. There is no straightforward way for a code generator to interpret what anyOf means, which can lead to undefined or unintended behavior or simply any schema being allowed. We’ll dig into this more later.

Recommended Practices

When you’re writing your OAD, you need to consider your end goals. The distinctions between allOf, oneOf, anyOf are subtle, but the implications on types in a generated SDK can be huge. To avoid downstream problems, we recommend following these rules:

  • Use oneOf to represent union type object fields.
  • Use allOf to represent intersection type / composite objects and fields.
  • Don’t use anyOf unless you absolutely need to.

Below, we will step through each of the different keywords and explain how to use formats, patterns, and additional attributes to give you a spec that is descriptive and explicit.

What is oneOf?

The oneOf keyword in JSON Schema and OpenAPI specifies that a value must match exactly one from a given set of schemas.

oneOf is the closest OpenAPI analog to the concept of a union type. A union type is a way to declare a variable or parameter that can hold values of multiple different types. They allow you to make your code more flexible while still providing type safety to users.

Let’s look at an example of how a oneOf is translated into a typescript object:


components:
schemas:
Drink:
type: object
oneOf:
- $ref: "#/components/schemas/Cocktail"
- $ref: "#/components/schemas/Mocktail"

That would produce a type structure like:


type Drink = Cocktail | Mocktail;

What is allOf?

The allOf keyword in JSON Schema and OpenAPI combines multiple schemas to create a single object that must be valid against all of the given subschemas.

allOf is the closest OpenAPI analog to an intersection type or a composite data type. You can use allOf to create a new type by combining multiple existing types. The new type has all the features of the existing types.


components:
schemas:
MealDeal:
type: object
allOf:
- $ref: "#/components/schemas/Cocktail"
- $ref: "#/components/schemas/Snack"

That would produce a type structure like:


type MealDeal = Cocktail & Snack;

Pitfall: Construction of Illogical Schemas

allOf has valid use cases, but you can also shoot yourself in the foot fairly easily. The most common problem that occurs when using allOf is the construction of an illogical schema. Consider the following example:


type: object
properties:
orderId:
description: ID of the order.
type: integer
allOf:
- $ref: '#/components/schemas/MealDealId'
...
components:
schemas:
MealDealId:
type: string
description: The id of a meal deal.

The OAS itself doesn’t mandate type validation, so this is technically valid. However, if you try to turn this into functional code, you will quickly realize that you’re trying to make something both an integer and a string at the same time, something that is clearly not possible.

Speakeasy’s implementation of allOf is a work in progress. To avoid the construction of illogical types, we currently construct an object using the superset of fields from the listed schemas. In cases where the base schemas have a collision, we will default to using the object deepest in the list.

What is anyOf?

The anyOf keyword in JSON Schema and OpenAPI is the poor misunderstood sibling of oneOf and allOf. There is no established convention about how anyOf should be interpreted, which often leads to some very nasty unintended behavior. The issue arises when anyOf is interpreted to mean that a value must match at least one of the given listed schemas.

There could be a valid use of anyOf to describe an extended match of one element of a list. But that is not currently implemented by any OpenAPI tooling known to us.

Pitfall: Combinatorial Explosion of Type

anyOf leads to a lot of problems in code generation because, taken literally, it describes a combinatorial number of data types. Imagine the following object definition:


components:
schemas:
Drink:
type: object
anyOf:
- $ref: "#/components/schemas/Soda"
- $ref: "#/components/schemas/Water"
- $ref: "#/components/schemas/Wine"
- $ref: "#/components/schemas/Spirit"
- $ref: "#/components/schemas/Beer"

To avoid the explosion of types described below, Speakeasy’s SDK creation interprets anyOf as oneOf.

If you’re doing code generation, you need to explicitly build types to cover all the possible combinations of these 5 liquids (even though most would be disgusting). That would lead you to build over 200 types to cover all the different combinations. That would lead to tremendous bloat in your library. That’s why our recommendation is don’t use anyOf.

Describing Nullable Objects

People sometimes incorrectly use oneOf when they want to indicate that it is possible for an object to be null. It differs based on the the version of OpenAPI you are using, but there are better ways to describe something as nullable.

If you are using OpenAPI 3.0 use the nullable property:


components:
schemas:
Drink:
type: object
nullable: true

If you are using OpenAPI 3.1, use type: [’object’, ‘null’] to specify that an object is nullable:


components:
schemas:
Drink:
type: [object, 'null']

Conclusion

AnyOf, AllOf, and OneOf are powerful keywords that can be used to define the structure and validation rules for data in OpenAPI.

You’ll notice that this article doesn’t cover the JSON Schema notkeyword. Although this keyword is valid in OAS, its use with code-generation tools leads to immediate problems. How can a code generator generate code for every possible schema except one or a set? This problem has taxed many big-brains, and remains unsolved today.

Here is a link to a blog post that provides more information about defining data types in OpenAPI:

https://speakeasyapi.dev/post/openapi-tips-data-type-formats/ (opens in a new tab)

CTA background illustrations

Speakeasy Changelog

Subscribe to stay up-to-date on Speakeasy news and feature releases.