OneOf, AllOf, AnyOf Oh my! How to define union types in OpenAPI
Tags: openapi, api
Published: 2023-10-30 16:12:14

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:

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

That would produce a type structure like:

```tsx
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.

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

That would produce a type structure like:

```tsx
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:

```yaml
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:

```tsx
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:

```yaml
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:

```yaml
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/