OneOf, AllOf, AnyOf Oh my! How to define union types in OpenAPI
October 19, 2023
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.
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
oneOfkeyword 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
intfor a certain field depending on the use case,
oneOfwould be used. In code generation, it will be generally interpreted as a union type.
allOf: The value must match all of the subschemas. The
allOfkeyword 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
anyOfkeyword 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
anyOfmeans, which can lead to undefined or unintended behavior or simply any schema being allowed. We’ll dig into this more later.
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:
oneOfto represent union type object fields.
allOfto represent intersection type / composite objects and fields.
- Don’t use
anyOfunless 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.
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: objectoneOf:- $ref: "#/components/schemas/Cocktail"- $ref: "#/components/schemas/Mocktail"
That would produce a type structure like:
type Drink = Cocktail | Mocktail;
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: objectallOf:- $ref: "#/components/schemas/Cocktail"- $ref: "#/components/schemas/Snack"
That would produce a type structure like:
type MealDeal = Cocktail & Snack;
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: objectproperties:orderId:description: ID of the order.type: integerallOf:- $ref: '#/components/schemas/MealDealId'...components:schemas:MealDealId:type: stringdescription: 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
allOfis 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.
anyOf keyword in JSON Schema and OpenAPI is the poor misunderstood sibling of
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
anyOfto describe an extended match of one element of a list. But that is not currently implemented by any OpenAPI tooling known to us.
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: objectanyOf:- $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
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.
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: objectnullable: 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']
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: