OpenAPI Tips
OneOf, AllOf, AnyOf Oh my! How to define union types in OpenAPI
Nolan Sullivan
October 19, 2023
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. TheoneOf
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 astring
or anint
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. TheallOf
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. TheanyOf
keyword is useful for describing type validation (similar tooneOf
), but it can get you into a lot of trouble in code generation. There is no straightforward way for a code generator to interpret whatanyOf
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: objectproperties: 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
asoneOf
.
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 not
keyword. 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)