JSON 08 📄 Schema Composition — allOf, anyOf, oneOf, and not
The composition keywords are the boolean algebra of JSON Schema. They let a schema combine subschemas with the logical operators—the AND, the OR, the XOR, and the NOT—and the combination is the mechanism that expresses constraints a single schema cannot. The allOf is the conjunction, the anyOf is the disjunction, the oneOf is the exclusive disjunction, and the not is the negation . The four keywords are the complete set, and the combination is the mechanism that makes complex schemas composable from small ones.
The composition keywords do not behave like inheritance in object-oriented languages. The subschemas inside an allOf know nothing of one another, and the additionalProperties: false inside one subschema does not see the properties declared in the sibling subschemas . The independence is the source of the most common surprise, and the unevaluatedProperties and the unevaluatedItems are the 2020-12 keywords that address the gap. The composition is the logical combination, not the inheritance, and the distinction is the mechanism that makes the schema predictable.
The 2020-12 release also made the composition behaviour more precise by defining the “evaluated” annotations. When a subschema successfully validates an instance property, the property is marked as evaluated, and the unevaluatedProperties keyword applies the strictness only to the properties that no subschema evaluated . This is the mechanism that lets the allOf with the sibling property declarations and the strict object coexist, which was impossible with the additionalProperties alone.
Key point: allOf requires the instance to validate against every subschema. anyOf requires at least one. oneOf requires exactly one. not requires the instance to fail the subschema. The subschemas are independent, and the additionalProperties in one subschema does not see the properties declared in the others. The unevaluatedProperties is the 2020-12 fix for the strict objects.
Why the composition keywords exist
The single-schema problem. A schema describes one set of constraints, and the type keyword describes one type. A value that can be either a string of at most five characters or a non-negative number needs the two constraints, and the anyOf is the mechanism that combines them . The composition is the mechanism that expresses the alternatives and the conjunctions that a single schema cannot.
The boolean problem. The constraints are the assertions, and the assertions combine with the AND, the OR, the XOR, and the NOT. The allOf is the AND, the anyOf is the OR, the oneOf is the XOR, and the not is the NOT . The composition is the boolean algebra, and the algebra is the mechanism that makes the complex constraints expressible.
The independence problem. The subschemas inside the composition are evaluated independently. The allOf with the two object subschemas does not merge the properties, and the additionalProperties: false in one subschema rejects the properties declared in the other . The independence is the source of the surprise, and the unevaluatedProperties is the 2020-12 mechanism that applies the strictness after all the subschemas have evaluated.
The annotation problem. The composition keywords produce the annotations as well as the validation results. When a subschema validates an instance, it marks the property or the item as evaluated, and the parent schema can use the annotation. The unevaluatedProperties and the unevaluatedItems are the keywords that read the annotations, and the $ref is the mechanism that produces them from the referenced schema . The annotation is the mechanism that makes the composition composable with the strictness.
The performance problem. The oneOf requires the verification of every subschema to confirm the exclusivity. The validator cannot short-circuit after the first match, because a second match would make the instance invalid . The anyOf can short-circuit after the first match, and the difference is the performance cost of the oneOf over the anyOf.
The trade-off. The composition is powerful, and the power is the complexity. The illogical schemas—the allOf with the type: "string" and the type: "number"—are the logical impossibilities that validate nothing . The composition is the tool, and the discipline is the careful use.
a. The allOf and the anyOf
The allOf requires the instance to validate against every subschema. The value is an array of the schemas, and the instance is valid if it is valid against all of them.
// The allOf
{
"allOf": [
{ "type": "string" },
{ "maxLength": 5 }
]
}
The "short" is valid because it is a string and its length is at most five. The "too long" is invalid because its length is greater than five . The allOf cannot be used to extend a schema in the inheritance sense, because the instance must independently validate against every subschema. The logical AND is the constraint, and the subschemas are the operands.
The anyOf requires the instance to validate against at least one subschema. The value is an array of the schemas, and the instance is valid if it is valid against any of them.
// The anyOf
{
"anyOf": [
{ "type": "string", "maxLength": 5 },
{ "type": "number", "minimum": 0 }
]
}
The "short" is valid because it matches the first subschema. The 12 is valid because it matches the second. The -5 is invalid because it matches neither . The anyOf is the OR, and the instance needs only one match. The validator can short-circuit after the first successful subschema, which is the performance advantage over the oneOf.
The allOf and the anyOf are the common pair for the union and the intersection of the constraints. The type: ["string", "number"] is the shorthand for the anyOf with the two type subschemas, and the allOf is the mechanism for the multiple independent constraints on the same type.
b. The oneOf and the not
The oneOf requires the instance to validate against exactly one subschema. The value is an array of the schemas, and the instance is valid if it is valid against one and only one.
// The oneOf
{
"oneOf": [
{ "type": "number", "multipleOf": 5 },
{ "type": "number", "multipleOf": 3 }
]
}
The 10 is valid because it is a multiple of 5 and not 3. The 9 is valid because it is a multiple of 3 and not 5. The 15 is invalid because it is a multiple of both 5 and 3 . The oneOf is the XOR, and the instance must match one and only one. The validator must check every subschema to confirm the exclusivity, and the check is the performance cost.
The not requires the instance to fail the subschema. The value is a single schema, and the instance is valid if it is not valid against the schema.
// The not
{
"not": { "type": "string" }
}
The 42 is valid because it is not a string. The {"key": "value"} is valid because it is not a string. The "I am a string" is invalid because it is a string . The not is the NOT, and the instance must fail the subschema. The double negation is the common confusion: the not with the not is the positive constraint, and the two negations cancel.
The oneOf has the performance cost, because the validator must check every subschema to confirm the exclusivity. The anyOf is the cheaper alternative when the exclusivity is not required . The not is the cheapest, because it only needs to evaluate the one subschema and negate the result.
Complete Example Session
// ============================================
// PART 1: THE ALL OF
// ============================================
{
"allOf": [
{ "type": "string" },
{ "maxLength": 5 }
]
}
// The "short" is valid.
// The "too long" is invalid.
// ============================================
// PART 2: THE ANY OF
// ============================================
{
"anyOf": [
{ "type": "string", "maxLength": 5 },
{ "type": "number", "minimum": 0 }
]
}
// The "short" is valid.
// The 12 is valid.
// The -5 is invalid.
// ============================================
// PART 3: THE ONE OF
// ============================================
{
"oneOf": [
{ "type": "number", "multipleOf": 5 },
{ "type": "number", "multipleOf": 3 }
]
}
// The 10 is valid.
// The 9 is valid.
// The 15 is invalid (multiple of both).
// ============================================
// PART 4: THE NOT
// ============================================
{
"not": { "type": "string" }
}
// The 42 is valid.
// The "I am a string" is invalid.
// ============================================
// PART 5: THE ILLOGICAL SCHEMA
// ============================================
{
"allOf": [
{ "type": "string" },
{ "type": "number" }
]
}
// Nothing is valid.
// The schema is a logical impossibility.
// ============================================
// PART 6: THE SUBSCHEMA INDEPENDENCE
// ============================================
{
"allOf": [
{ "type": "object", "properties": { "a": { "type": "string" } } },
{ "type": "object", "properties": { "b": { "type": "number" } } }
]
}
// The { "a": "x", "b": 1 } is valid.
// The subschemas do not know of one another.
// The properties are not merged.
// ============================================
// PART 7: THE ADDITIONAL PROPERTIES TRAP
// ============================================
{
"allOf": [
{ "properties": { "a": { "type": "string" } } },
{ "additionalProperties": false }
]
}
// The { "a": "x" } is invalid.
// The additionalProperties knows nothing of the "a" in the sibling.
// The 2020-12 unevaluatedProperties is the fix.
// ============================================
// PART 8: THE UNEVALUATED PROPERTIES
// ============================================
{
"allOf": [
{ "properties": { "a": { "type": "string" } } }
],
"unevaluatedProperties": false
}
// The { "a": "x" } is valid.
// The unevaluatedProperties sees the evaluated "a".
// The { "a": "x", "b": 1 } is invalid.
// ============================================
// PART 9: THE FACTORING
// ============================================
// The two schemas are equivalent:
{
"type": "number",
"oneOf": [
{ "multipleOf": 5 },
{ "multipleOf": 3 }
]
}
{
"oneOf": [
{ "type": "number", "multipleOf": 5 },
{ "type": "number", "multipleOf": 3 }
]
}
// The common part is factored out.
// ============================================
// PART 10: THE CHECKLIST
// ============================================
// [ ] The allOf is the AND.
// [ ] The anyOf is the OR.
// [ ] The oneOf is the XOR.
// [ ] The not is the NOT.
// [ ] The subschemas are independent.
// [ ] The additionalProperties does not see the siblings.
// [ ] The unevaluatedProperties is the 2020-12 fix.
// [ ] The factoring is used for the common parts.
The ten parts covered the allOf, the anyOf, the oneOf, the not, the illogical schema, the subschema independence, the additionalProperties trap, the unevaluatedProperties, the factoring, and the checklist. The through-line is that the composition is the boolean algebra, and the subschemas are the independent assertions.
Quick Reference
The composition keywords
| Keyword | Boolean | Requirement |
|---|---|---|
allOf | AND | Valid against all the subschemas |
anyOf | OR | Valid against at least one |
oneOf | XOR | Valid against exactly one |
not | NOT | Not valid against the subschema |
The subschema independence
| Aspect | Behavior |
|---|---|
| The subschemas | Evaluated independently |
The additionalProperties | Does not see the sibling properties |
The unevaluatedProperties | Sees the evaluated properties |
The common patterns
| Pattern | Schema |
|---|---|
| The string or number | anyOf: [{ type: "string" }, { type: "number" }] |
| The string with a length | allOf: [{ type: "string" }, { maxLength: 5 }] |
| The exclusive multiple | oneOf: [{ multipleOf: 5 }, { multipleOf: 3 }] |
| The not a string | not: { type: "string" } |
The performance order
| Keyword | Short-circuit | Cost |
|---|---|---|
anyOf | Yes | The lowest |
not | Yes | The low |
allOf | No | The medium |
oneOf | No | The highest |
Best Practices
✅ Do This:
// Use the allOf for the conjunction
{ "allOf": [{ "type": "string" }, { "maxLength": 5 }] } // ✅
// Use the anyOf for the disjunction
{ "anyOf": [{ "type": "string" }, { "type": "number" }] } // ✅
// Use the unevaluatedProperties for the strict objects
{ "allOf": [{ "properties": { "a": {} } }], "unevaluatedProperties": false } // ✅
❌ Don’t Do This:
// Use the additionalProperties in the allOf
{ "allOf": [{ "properties": { "a": {} } }, { "additionalProperties": false }] } // ❌
// Use the oneOf when the anyOf is enough
{ "oneOf": [{ "type": "string" }, { "type": "number" }] } // the extra verification // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
The allOf with additionalProperties rejects everything | The subschemas are independent | Use unevaluatedProperties |
The oneOf is slow | Every subschema is verified | Use anyOf when possible |
| The illogical schema validates nothing | The contradictory constraints | Check the type consistency |
The not is confusing | The double negation | Use the positive constraint |
Real-World Examples
1. The allOf
{ "allOf": [{ "type": "string" }, { "maxLength": 5 }] }
2. The anyOf
{ "anyOf": [{ "type": "string" }, { "type": "number" }] }
3. The oneOf
{ "oneOf": [{ "multipleOf": 5 }, { "multipleOf": 3 }] }
4. The not
{ "not": { "type": "string" } }
5. The type union shorthand
{ "type": ["string", "number"] }
6. The unevaluatedProperties
{ "allOf": [{ "properties": { "a": {} } }], "unevaluatedProperties": false }
7. The factoring
{ "type": "number", "oneOf": [{ "multipleOf": 5 }, { "multipleOf": 3 }] }
8. The additionalProperties trap
{ "allOf": [{ "properties": { "a": {} } }, { "additionalProperties": false }] }
9. The $ref in the allOf
{ "allOf": [{ "$ref": "#/$defs/base" }, { "required": ["id"] }] }
10. The if-then-else composition
{ "if": { "properties": { "type": { "const": "a" } } }, "then": { "required": ["a"] } }
Visual
The Boolean Algebra
┌──────────────────────────────────────────────────────────────┐
│ THE COMPOSITION KEYWORDS │
│ │
│ allOf (AND) │
│ ┌─────────────────────┐ │
│ │ A ∧ B ∧ C │ All the subschemas must validate │
│ └─────────────────────┘ │
│ │
│ anyOf (OR) │
│ ┌─────────────────────┐ │
│ │ A ∨ B ∨ C │ At least one must validate │
│ └─────────────────────┘ │
│ │
│ oneOf (XOR) │
│ ┌─────────────────────┐ │
│ │ A ⊕ B ⊕ C │ Exactly one must validate │
│ └─────────────────────┘ │
│ │
│ not (NOT) │
│ ┌─────────────────────┐ │
│ │ ¬A │ The subschema must not validate │
│ └─────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
The Subschema Independence
┌──────────────────────────────────────────────────────────────┐
│ THE SUBSCHemas DO NOT SHARE THE STATE │
│ │
│ allOf: │
│ ┌─────────────────────┐ │
│ │ Sub A │ The properties: a │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ Sub B │ The additionalProperties: false │
│ │ │ Does not see the "a" of Sub A │
│ └─────────────────────┘ │
│ │
│ The unevaluatedProperties sees the evaluated "a". │
└──────────────────────────────────────────────────────────────┘
The Performance Order
┌──────────────────────────────────────────────────────────────┐
│ THE SHORT-CIRCUIT │
│ │
│ anyOf: │
│ ┌─────────────────────┐ │
│ │ Check A │ If valid, the result is valid │
│ │ Check B │ Only if A fails │
│ └─────────────────────┘ │
│ │
│ oneOf: │
│ ┌─────────────────────┐ │
│ │ Check A │ Must check B even if A is valid │
│ │ Check B │ To confirm the exclusivity │
│ └─────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
allOf | The AND, all the subschemas |
anyOf | The OR, at least one |
oneOf | The XOR, exactly one |
not | The NOT, fails the subschema |
| The independence | The subschemas do not share the state |
The unevaluatedProperties | The 2020-12 strictness fix |
| The factoring | The common parts extracted |
Key takeaways:
allOfis the AND,anyOfis the OR,oneOfis the XOR, andnotis the NOT. The four keywords are the boolean algebra of the schema composition, and the combination is the mechanism that expresses the complex constraints.- The subschemas are independent. The
allOfdoes not merge the properties, and theadditionalProperties: falsein one subschema does not see the properties declared in the other. TheunevaluatedPropertiesis the 2020-12 keyword that applies the strictness after all the subschemas have evaluated. oneOfis the expensive XOR. The validator must check every subschema to confirm the exclusivity, and theanyOfis the cheaper alternative when the exclusivity is not required.- The illogical schemas are the logical impossibilities. The
allOfwith thetype: "string"and thetype: "number"validates nothing, and the schema is the contradiction. - The factoring is the common-part extraction. The
type: "number"outside theoneOfand thetype: "number"inside each branch are the equivalent schemas.
Remember: The composition keywords are the boolean algebra. The allOf is the AND, the anyOf is the OR, the oneOf is the XOR, and the not is the NOT. The subschemas are independent, and the unevaluatedProperties is the 2020-12 fix for the strict objects. The oneOf is the expensive exclusivity, and the anyOf is the cheaper alternative. The composition is the tool, and the independence is the discipline.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!