OpenAPI's `oneOf` and `anyOf` keywords define polymorphic APIs where objects can take different shapes, but choosing the wrong one creates confusing validation errors and ambiguous SDKs. Getting this distinction right is critical for API providers who want to generate strongly-typed SDKs that prevent runtime errors and provide clear developer experiences.
This guide covers when to use oneOf
versus anyOf
, how discriminators eliminate ambiguity in polymorphic schemas, and practical examples for modeling payment methods, account types, and notification preferences. You'll learn best practices for discriminator implementation and see how proper schema design translates into type-safe, idiomatic SDKs across multiple programming languages.
Define oneOf and anyOf in OpenAPI
In OpenAPI, oneOf
and anyOf
are keywords used to define polymorphic APIs, where a single object might take one of several different shapes. The oneOf
keyword is exclusive, meaning it validates that the data matches exactly one of the specified subschemas. In contrast, anyOf
is inclusive, validating if the data matches one or more of the subschemas.
For example, if a Pet
can be either a Cat
or a Dog
but never both at once, you would use oneOf
. If a Notification
can be sent via Email
, SMS
, or both simultaneously, you would use anyOf
.
Getting this right is critical for creating a predictable API and for tools that generate strongly-typed SDKs, which starts when you create OpenAPI specs with clear schema definitions. A good generator like the Stainless SDK generator reads these keywords to build precise types, like tagged unions, which prevent developers from trying to access properties that don't exist on a specific object type.
Choose oneOf or anyOf with discriminators
The choice between oneOf
and anyOf
comes down to whether your object shapes are mutually exclusive. If an object can only ever be one of the possible types, oneOf
is the correct choice. If its shape can conform to multiple types at the same time, use anyOf
.
Using the wrong keyword can lead to confusing validation errors or ambiguous SDKs. Well-configured SDK generators will often raise a diagnostic warning when a schema choice is ambiguous, prompting you to add clarity for better type safety and making it easier to integrate SDK snippets with your API docs.
Keyword | Use Case | Validation Rule |
---|---|---|
| Shapes are mutually exclusive (e.g., | Must match exactly one subschema. |
| Shapes can overlap (e.g., | Must match at least one subschema. |
Add discriminators for clear polymorphism
A discriminator is an object that helps tools and developers determine which specific schema is being used when oneOf
or anyOf
is present. It points to a specific property in the payload, like type: 'card'
, which acts as a key to identify the correct schema. This removes ambiguity and enables powerful features like type narrowing in modern programming languages.
The discriminator object has two key fields:
propertyName
: The name of the field in your object that holds the identifying value (e.g.,type
). This property must be marked asrequired
in each subschema.mapping
: An optional map that links the values in thepropertyName
field to specific schema definitions.
You can use either implicit or explicit mapping. Implicit mapping is simpler but can be brittle, while explicit mapping is more robust and recommended for production APIs.
Use implicit mapping
With implicit mapping, the value of the discriminator property must match the name of the schema in #/components/schemas/
. For example, if the discriminator property objectType
has a value of "Cat", the validator will look for a schema named Cat
.
This generates a Pet
union type in an SDK, where you can check pet.objectType
to know if you have a Cat
or a Dog
.
Use explicit mapping
Explicit mapping gives you full control by defining which value maps to which schema. This is safer because it decouples the public API value from your internal schema names, allowing you to refactor them without causing a breaking change.
Here, a payload with objectType: 'cat'
will correctly validate against the Feline
schema.
Model payment methods with oneOf
A common real-world use case for oneOf
is modeling different payment methods. A single charge can be paid with a credit card, a bank transfer, or a digital wallet, but never more than one at the same time. This exclusivity makes oneOf
the perfect fit.
We'll use a discriminator with propertyName: method
to identify the payment type.
Optional: Stainless config snippet
Generated TypeScript client:
Send card payment
A request using a card would include the method: 'card'
discriminator and card-specific fields.
Request Payload:
SDK Usage (TypeScript):
Send bank transfer
Similarly, a bank transfer request specifies method: 'bank_transfer'
and its relevant details.
Request Payload:
SDK Usage (Python):
Model account types with oneOf
Another great example is modeling different account types, like Personal
and Business
. Both might share common base properties like an id
and email
, but have distinct properties for things like tax identifiers.
Here, we can use allOf
to define a BaseAccount
and oneOf
with a discriminator to handle the variants.
A good code generator produces an Account
type that correctly inherits from a base class or interface, making the SDK feel natural to use.
Create personal account
The payload must include account_type: 'personal'
and the personal_tax_id
.
Request Payload:
Create business account
The payload for a business account requires account_type: 'business'
and the business_tax_id
.
Request Payload:
Model notification settings with anyOf
Now let's look at anyOf
. Imagine a user can enable multiple notification channels simultaneously: email, SMS, and push notifications. Since any combination is valid, anyOf
is the right choice.
While a discriminator isn't strictly required for anyOf
to be valid, including one can help tooling and makes the intent clearer.
Enable single channel
A payload with just one channel type is valid.
Request Payload:
Enable multiple channels
A payload with multiple channel types is also valid because anyOf
allows matching against more than one subschema.
Request Payload:
See SDK impact of discriminators
The real magic of discriminators appears in the generated SDK. They enable type-safe patterns that eliminate entire classes of bugs and dramatically improve the developer experience.
When an SDK is generated from a spec with discriminators, developers can use standard language features to safely access properties of a specific type.
Improve editor hints
With a discriminated union, your code editor understands the different possible shapes. When you check the discriminator property, the editor's autocomplete will only suggest properties that are valid for that specific type.
TypeScript Example:
Prevent runtime errors
This pattern also introduces compile-time safety. Trying to access a property from the wrong variant will result in a type error before you even run your code, preventing runtime exceptions.
Python Example:
For advanced use cases, like serving APIs to AI clients with varying schema support, it's even possible to configure generation to flatten or transform these unions on the fly to ensure compatibility, similar to the transformations needed when converting complex OpenAPI specs to MCP servers.
Follow best practices for discriminator schemas
To get the most out of discriminators and avoid common pitfalls, follow these best practices. They will ensure your API is robust, predictable, and easy for both humans and machines to work with.
Require the discriminator property: Always include the
propertyName
in therequired
list of every subschema to ensure the identifying field is always present.Keep discriminator values stable: Treat the values in your discriminator field as part of your API contract. Changing them is a breaking change, similar to the considerations when making Java enums forwards compatible.
Avoid case-sensitivity bugs: Stick to a consistent casing convention like
snake_case
for discriminator values to prevent subtle bugs.Use explicit mapping in production: While implicit mapping is convenient, explicit mapping is safer against accidental breaking changes from schema refactoring.
Avoid missing property errors
If the discriminator property is not marked as required
, a client could send a payload without it. This makes it impossible for validators and tools to determine the object's type, leading to validation failures.
Incorrect (Missing required
):
Correct:
Avoid name collisions
If your discriminator propertyName
(e.g., type
) collides with an existing business logic field, it can cause confusion. A common strategy is to use a more specific name like object_type
or _type
to avoid ambiguity.
Frequently asked questions about OpenAPI discriminators
How does Stainless generate SDKs for discriminator schemas?
Our generator creates language-native polymorphic types, such as tagged unions in TypeScript or class hierarchies in Python and Java. This allows for static type checking and improves developer ergonomics.
Can I use discriminators without oneOf or anyOf?
While the OpenAPI specification allows a discriminator
to be defined on any schema, it is only functionally active when used in combination with oneOf
, anyOf
, or allOf
.
How do clients with limited schema support handle discriminators?
A robust generation process can include a client-capability flag to automatically rewrite schemas, or you can add custom code that persists through regeneration for specific client requirements. This can involve inlining $ref
s or simplifying unions to ensure broad compatibility.
What happens if multiple subschemas match in oneOf?
This is a validation error. The purpose of oneOf
is to enforce that the data matches exactly one schema, and a good validator will reject any data that matches more than one.
Should I prefer implicit or explicit mapping?
For production APIs, explicit mapping is strongly recommended. It decouples the public API values from your internal schema organization, making your API more resilient to refactoring.
Ready to build high-quality, idiomatic SDKs from your OpenAPI spec? Get started for free.