Use of oneOf
and anyOf
comes from the need to describe data that can take multiple different forms. When you need to validate against alternative schemas, oneOf
should be your preferred approach.
This makes sense. You might want to handle different payload structures, alternative response formats, or polymorphic data. oneOf
provides clear, predictable validation where data must conform to exactly one well-defined schema.
anyOf
, while available, creates parsing ambiguity and unpredictable outcomes when multiple schemas match. Before reaching for anyOf
, consider refining your schema design to use oneOf
with proper discriminators.
How do you know when to use oneOf
and when schema redesign is needed? This article covers:
- how to use
oneOf
andanyOf
- why
oneOf
provides better predictability - valid use cases and schema design patterns
- when
anyOf
might be unavoidable (and its trade-offs)
Both oneOf
and anyOf
are declared as arrays of schemas, similar to allOf
.
All of these keywords must be set to an array, where each item is a schema.
This works in YAML.
oneOf:
- title: CreditCard
type: object
properties:
type:
type: string
enum: [credit]
cardNumber:
type: string
cvv:
type: string
required:
- type
- cardNumber
- cvv
- title: BankAccount
type: object
properties:
type:
type: string
enum: [bank]
accountNumber:
type: string
routingNumber:
type: string
required:
- type
- accountNumber
- routingNumber
And it works in JSON. The remainder of this article uses YAML for schema definitions.
{
"oneOf": [
{
"title": "CreditCard",
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["credit"]
},
"cardNumber": {
"type": "string"
},
"cvv": {
"type": "string"
}
},
"required": ["type", "cardNumber", "cvv"]
},
{
"title": "BankAccount",
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["bank"]
},
"accountNumber": {
"type": "string"
},
"routingNumber": {
"type": "string"
}
},
"required": ["type", "accountNumber", "routingNumber"]
}
]
}
anyOf:
- title: HasEmail
type: object
properties:
email:
type: string
format: email
required:
- email
- title: HasPhone
type: object
properties:
phone:
type: string
required:
- phone
A goal of JSON Schema is to be able to evaluate if JSON is valid or invalid with the defined schema. More importantly, the validation outcome should be predictable and unambiguous for API consumers.
From the definition of oneOf
, it is treated like an exclusive OR (XOR):
Must be valid against exactly one of the subschemas
($creditCard && !$bankAccount) || (!$creditCard && $bankAccount)
Based on our prior oneOf
declaration for payment methods, the following JSON would match exactly one schema (CreditCard):
{
"type": "credit",
"cardNumber": "4111111111111111",
"cvv": "123"
}
The discriminator field (type
) makes it crystal clear which schema applies. Code generators, documentation tools, and API consumers can reliably determine the data structure.
Note on terminology: We use "discriminator property" to mean any property that helps distinguish between schemas (like our type
field with enum values). This is different from OpenAPI's formal discriminator
object, which is optional tooling enhancement. You don't need the specification's discriminator
feature - just well-designed properties that clearly differentiate your schemas.
Important: JSON schemas default to additionalProperties: true
, meaning extra properties are allowed. Without proper discriminating properties or additionalProperties: false
, seemingly different schemas can both match the same data, breaking oneOf
validation.
What about this JSON that could theoretically match both schemas?
{
"type": "credit",
"cardNumber": "4111111111111111",
"cvv": "123",
"accountNumber": "123456789",
"routingNumber": "987654321"
}
With the discriminator enum values ([credit]
vs [bank]
), this clearly matches only the CreditCard schema because type: "credit"
. The extra bank properties (accountNumber
, routingNumber
) would be allowed as additional properties but don't cause schema collision.
However, without discriminators, this becomes problematic:
{
"cardNumber": "4111111111111111",
"cvv": "123",
"accountNumber": "123456789"
}
If the schemas lacked discriminator enums, this JSON could match both schemas due to additionalProperties: true
(the OpenAPI default), making the oneOf
invalid. This is why discriminators and explicit additionalProperties
control are essential.
From the definition of anyOf
, it is treated like an inclusive OR:
Must be valid against at least one of the subschemas
$hasEmail || $hasPhone
The problem: When multiple schemas match, which one does the parser use? The behavior is undefined.
Based on an anyOf
declaration for contact methods, all of these JSON examples would be valid:
{
"email": "user@example.com"
}
{
"phone": "+1-555-0123"
}
{
"email": "user@example.com",
"phone": "+1-555-0123"
}
The last example is problematic: it matches both schemas, but different tools might:
- Use the first matching schema
- Use the last matching schema
- Merge properties from all matching schemas
- Fail to generate code predictably
This unpredictability makes anyOf
unsuitable for most API design scenarios.
Always start with oneOf
. It provides clear, predictable validation where data must conform to exactly one well-defined schema.
Polymorphic types with discriminators:
oneOf:
- title: Painting
type: object
properties:
artworkType:
type: string
enum: [painting]
artist:
type: string
medium:
type: string
dimensions:
type: string
required:
- artworkType
- artist
- title: Sculpture
type: object
properties:
artworkType:
type: string
enum: [sculpture]
artist:
type: string
material:
type: string
weight:
type: number
required:
- artworkType
- artist
Different response formats:
oneOf:
- title: SuccessResponse
type: object
properties:
status:
type: string
enum: [success]
data:
type: object
required:
- status
- data
- title: ErrorResponse
type: object
properties:
status:
type: string
enum: [error]
message:
type: string
required:
- status
- message
Avoid anyOf
when possible. Only consider it when schema redesign with oneOf
is truly impossible.
The few legitimate cases are typically constraint validation rather than structural alternatives:
Password strength validation (acceptable use):
title: Password
type: string
anyOf:
- minLength: 8
- pattern: "^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])"
- pattern: "^(?=.*[!@#$%^&*])"
❌ Avoid this pattern:
# DON'T: Unpredictable structure validation
title: Contact
type: object
properties:
name:
type: string
anyOf:
- properties:
email:
type: string
format: email
required:
- email
- properties:
phone:
type: string
required:
- phone
✅ Better: Redesign with explicit structure:
# DO: Clear, predictable structure
title: Contact
type: object
properties:
name:
type: string
email:
type: string
format: email
phone:
type: string
anyOf:
- required: [email]
- required: [phone]
Even better, use oneOf
with contact method types:
title: Contact
type: object
properties:
name:
type: string
contactMethod:
oneOf:
- title: EmailContact
type: object
properties:
type:
type: string
enum: [email]
value:
type: string
format: email
required: [type, value]
- title: PhoneContact
type: object
properties:
type:
type: string
enum: [phone]
value:
type: string
required: [type, value]
Words that indicate you might be reaching for anyOf
when better schema design is needed:
- flexible
- optional alternatives
- multiple valid forms
- extensible
The following example demonstrates a schema design problem that makes developers think they need anyOf
:
# ❌ Poor design - creates false restriction
oneOf:
- type: object
properties:
email:
type: string
format: email
required:
- email
- type: object
properties:
phone:
type: string
required:
- phone
This schema rejects users who have both email and phone, which seems wrong.
❌ Don't default to anyOf
:
# AVOID: Unpredictable parsing behavior
anyOf:
- type: object
properties:
email:
type: string
format: email
required:
- email
- type: object
properties:
phone:
type: string
required:
- phone
✅ Better: Redesign the schema structure:
# DO: Explicit, predictable structure
type: object
properties:
email:
type: string
format: email
phone:
type: string
anyOf:
- required: [email]
- required: [phone]
✅ Best: Use discriminated union:
# BEST: Clear, unambiguous structure
type: object
properties:
preferredContact:
oneOf:
- title: EmailPreference
type: object
properties:
method:
type: string
enum: [email]
email:
type: string
format: email
required: [method, email]
- title: PhonePreference
type: object
properties:
method:
type: string
enum: [phone]
phone:
type: string
required: [method, phone]
# Optional additional contact methods
alternateEmail:
type: string
format: email
alternatePhone:
type: string
Note that having the same property with different types is perfectly valid for oneOf
:
# ✅ Valid oneOf - value can be string OR number
oneOf:
- type: object
properties:
value:
type: string
- type: object
properties:
value:
type: number
This works because oneOf
requires exactly one match:
- If
value
is a string, it matches the first schema only - If
value
is a number, it matches the second schema only
This would be illogical for allOf
:
# ❌ Illogical for allOf - nothing can be both string AND number
allOf:
- type: object
properties:
value:
type: string
- type: object
properties:
value:
type: number
The oneOf
version above is valid but could benefit from discriminating properties for better clarity:
# ✅ Even clearer with discriminating properties
oneOf:
- type: object
properties:
type:
type: string
enum: [text]
value:
type: string
required:
- type
- value
- type: object
properties:
type:
type: string
enum: [numeric]
value:
type: number
required:
- type
- value
Critical OpenAPI gotcha: By default, JSON Schema (and OpenAPI) schemas allow additional properties (additionalProperties: true
). This means seemingly different schemas can both validate the same JSON, breaking oneOf
.
# ❌ These schemas will collide due to additionalProperties: true (default)
oneOf:
- title: User
type: object
properties:
name:
type: string
email:
type: string
- title: Product
type: object
properties:
name:
type: string
price:
type: number
This JSON would invalidate the oneOf
because it matches both schemas:
{
"name": "Widget",
"email": "contact@example.com",
"price": 29.99
}
The User schema matches because it has name
and email
, and price
is allowed as an additional property. The Product schema matches because it has name
and price
, and email
is allowed as an additional property.
✅ Solution 1: Use additionalProperties: false
oneOf:
- title: User
type: object
properties:
name:
type: string
email:
type: string
additionalProperties: false
- title: Product
type: object
properties:
name:
type: string
price:
type: number
additionalProperties: false
✅ Solution 2: Use discriminator properties
oneOf:
- title: User
type: object
properties:
type:
type: string
enum: [user]
name:
type: string
email:
type: string
required: [type]
additionalProperties: false
- title: Product
type: object
properties:
type:
type: string
enum: [product]
name:
type: string
price:
type: number
required: [type]
additionalProperties: false
Always be explicit about additionalProperties
when using oneOf
.
The following example shows a truly illogical oneOf
where two schemas can both match the same data:
# ❌ Illogical - both schemas could match the same object
oneOf:
- type: object
properties:
name:
type: string
age:
type: number
required: [name]
- type: object
properties:
name:
type: string
email:
type: string
required: [name]
This JSON would be invalid because it matches both schemas:
{
"name": "John Smith",
"age": 30,
"email": "john@example.com"
}
Both schemas match because:
- First schema: has required
name
(✓) andage
is allowed as additional property - Second schema: has required
name
(✓) andemail
is allowed as additional property
Even this simpler case is invalid (often missed by developers):
{
"name": "John Smith",
"email": "john@example.com"
}
Developers often think "this clearly matches only the second schema because it has email
", but it actually matches both:
- First schema: has required
name
(✓) andemail
is allowed as additional property - Second schema: has required
name
(✓) andemail
property (✓)
Both cases violate oneOf
's requirement of matching exactly one schema.
When using oneOf
with similar schemas, always include discriminating properties:
# ❌ Ambiguous - both schemas could match the same data
oneOf:
- type: object
properties:
title:
type: string
yearCreated:
type: number
- type: object
properties:
title:
type: string
acquisitionPrice:
type: number
# ✅ Clear discrimination
oneOf:
- type: object
properties:
recordType:
type: string
enum: [artwork]
title:
type: string
yearCreated:
type: number
required:
- recordType
- type: object
properties:
recordType:
type: string
enum: [acquisition]
title:
type: string
acquisitionPrice:
type: number
required:
- recordType
Prefer oneOf
for predictable, unambiguous API schemas. Always include discriminating properties (like type
fields with enum values) to ensure clear validation outcomes.
Avoid anyOf
for structural validation. It creates parsing ambiguity and unpredictable tool behavior. Instead, redesign your schemas to use oneOf
with proper discriminating properties.
The rare valid uses of anyOf
are for constraint validation (like password rules), not structural alternatives.
Be aware of schema design anti-patterns:
- Reaching for
anyOf
when the real problem is poor schema structure - Missing discriminating properties that create ambiguous
oneOf
schemas - Forgetting that OpenAPI's default
additionalProperties: true
causes schema collisions - Conflicting property definitions that make validation impossible
When you think you need anyOf
, step back and redesign your schema structure. Clear, discriminated unions with oneOf
provide better developer experience and tool support.
The oneOf
keyword may optionally use OpenAPI's discriminator object for enhanced tooling support, but simple discriminating properties with enum values work perfectly well. Both oneOf
and discriminated unions work best with reference objects.
Remember: Predictable schemas lead to better APIs. Choose clarity over perceived flexibility.