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
oneOfandanyOf - why
oneOfprovides better predictability - valid use cases and schema design patterns
- when
anyOfmight 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
- routingNumberAnd 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:
- phoneA 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 || $hasPhoneThe 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
- artistDifferent 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
- messageAvoid 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:
- phoneThis 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: stringNote 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: numberThis works because oneOf requires exactly one match:
- If
valueis a string, it matches the first schema only - If
valueis 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: numberThe 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
- valueCritical 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: numberThis 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: falseAlways 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(✓) andageis allowed as additional property - Second schema: has required
name(✓) andemailis 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(✓) andemailis allowed as additional property - Second schema: has required
name(✓) andemailproperty (✓)
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:
- recordTypePrefer 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
anyOfwhen the real problem is poor schema structure - Missing discriminating properties that create ambiguous
oneOfschemas - Forgetting that OpenAPI's default
additionalProperties: truecauses 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.