OpenAPI oneOf vs AnyOf with Discriminator Examples Guide

OpenAPI oneOf vs AnyOf with discriminator examples: Compare exclusive vs overlapping schemas and show discriminators resolving ambiguity using YAML/JSON.

Jump to section

Jump to section

Jump to section

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

oneOf

Shapes are mutually exclusive (e.g., Card vs. BankTransfer)

Must match exactly one subschema.

anyOf

Shapes can overlap (e.g., EmailNotification vs. PushNotification)

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 as required in each subschema.

  • mapping: An optional map that links the values in the propertyName 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.

# OpenAPI Spec
Pet:
  oneOf:
    - $ref: '#/components/schemas/Cat'
    - $ref: '#/components/schemas/Dog'
  discriminator:
    propertyName

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.

# OpenAPI Spec
Pet:
  oneOf:
    - $ref: '#/components/schemas/Feline'
    - $ref: '#/components/schemas/Canine'
  discriminator:
    propertyName: objectType
    mapping:
      cat: '#/components/schemas/Feline'
      dog: '#/components/schemas/Canine'

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.

# OpenAPI Spec
components:
  schemas:
    Charge:
      type: object
      properties:
        amount:
          type: integer
        payment_method_details:
          oneOf:
            - $ref: '#/components/schemas/CardDetails'
            - $ref: '#/components/schemas/BankTransferDetails'
          discriminator:
            propertyName: method
            mapping:
              card: '#/components/schemas/CardDetails'
              bank_transfer: '#/components/schemas/BankTransferDetails'
    CardDetails:
      type: object
      required:
        - method
        - card_number
      properties:
        method:
          type: string
          enum: [card]
        card_number:
          type: string
    BankTransferDetails:
      type: object
      required:
        - method
        - account_number
      properties:
        method:
          type: string
          enum: [bank_transfer]
        account_number:
          type

Optional: Stainless config snippet

# stainless.yml
resources:
  charges:
    models:
      charge: '#/components/schemas/Charge'
    methods:
      create: post /charges

components:
  schemas:
    CardDetails: '#/components/schemas/CardDetails'
    BankTransferDetails: '#/components/schemas/BankTransferDetails'

Generated TypeScript client:

import { MyAppClient } from 'my-app';

const client = new MyAppClient({ authToken: process.env.API_KEY });
const charge = await client.charges.create({
  amount: 1000,
  payment_method_details: {
    method: 'card',
    card_number: '4242...',
  },
});

Send card payment

A request using a card would include the method: 'card' discriminator and card-specific fields.

Request Payload:

{
  "amount": 1000,
  "payment_method_details": {
    "method": "card",
    "card_number": "4242..."
  }
}

SDK Usage (TypeScript):

const charge = await client.charges.create({
  amount: 1000,
  payment_method_details: {
    method: 'card',
    card_number: '4242...',
  },
});

Send bank transfer

Similarly, a bank transfer request specifies method: 'bank_transfer' and its relevant details.

Request Payload:

{
  "amount": 2500,
  "payment_method_details": {
    "method": "bank_transfer",
    "account_number": "US12..."
  }
}

SDK Usage (Python):

charge = client.charges.create(
    amount=2500,
    payment_method_details={
        "method": "bank_transfer",
        "account_number": "US12...",
    },
)

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.

# OpenAPI Spec
components:
  schemas:
    Account:
      oneOf:
        - $ref: '#/components/schemas/PersonalAccount'
        - $ref: '#/components/schemas/BusinessAccount'
      discriminator:
        propertyName: account_type
    BaseAccount:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
    PersonalAccount:
      allOf:
        - $ref: '#/components/schemas/BaseAccount'
        - type: object
          properties:
            account_type:
              type: string
              enum: [personal]
            personal_tax_id:
              type: string
    BusinessAccount:
      allOf:
        - $ref: '#/components/schemas/BaseAccount'
        - type: object
          properties:
            account_type:
              type: string
              enum: [business]
            business_tax_id:
              type

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:

{
  "email": "[email protected]",
  "account_type": "personal",
  "personal_tax_id": "123-45-678"
}

Create business account

The payload for a business account requires account_type: 'business' and the business_tax_id.

Request Payload:

{
  "email": "[email protected]",
  "account_type": "business",
  "business_tax_id": "98-7654321"
}

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.

# OpenAPI Spec
NotificationPreferences:
  type: object
  properties:
    channels:
      type: array
      items:
        anyOf:
          - $ref: '#/components/schemas/EmailChannel'
          - $ref: '#/components/schemas/SmsChannel'

Enable single channel

A payload with just one channel type is valid.

Request Payload:

{
  "channels": [
    { "type": "email", "address": "[email protected]" }
  ]
}

Enable multiple channels

A payload with multiple channel types is also valid because anyOf allows matching against more than one subschema.

Request Payload:

{
  "channels": [
    { "type": "email", "address": "[email protected]" },
    { "type": "sms", "phone_number": "+15551234567" }
  ]
}

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:

if (charge.payment_method_details.method === 'card') {
  // Autocomplete suggests `card_number`, not `account_number`
  console.log(charge.payment_method_details.card_number);
}

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:

# This would raise a static analysis error
if charge.payment_method_details.method == "card":
    print(charge.payment_method_details.account_number) # Error: 'CardDetails' has no attribute 'account_number'

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 the required 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):

CardDetails:
  type: object
  properties:
    method:
      type: string
      enum

Correct:

CardDetails:
  type: object
  required:
    - method
  properties:
    method:
      type: string
      enum

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 $refs 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.