Ratgeber · JSON Schema 2020-12
Schema-Komposition: allOf, anyOf, oneOf - wann was?
allOf: alle müssen passen. anyOf: mindestens einer. oneOf: genau einer. Performance-Unterschiede und discriminator-Pattern zur Optimierung.
Drei Kompositions-Keywords
JSON Schema kennt vier Boolean-Operationen über Sub-Schemas: allOf, anyOf, oneOf, not. Die ersten drei sind die alltäglichen - not ist Spezialwerkzeug.
| Keyword | Match-Anforderung | Typischer Use-Case |
|---|---|---|
| allOf | Alle Sub-Schemas müssen passen | Mixin-Pattern, Schema-Erweiterung |
| anyOf | Mindestens eins muss passen | Offene Union-Typen |
| oneOf | Genau eins muss passen | Tagged Unions / Discriminated Unions |
allOf - Schema-Komposition durch Verkettung
Klassischer Use-Case: ein Basis-Schema und ein Erweiterungs-Schema kombinieren.
{
"$defs": {
"Timestamps": {
"type": "object",
"required": ["created_at", "updated_at"],
"properties": {
"created_at": { "type": "string", "format": "date-time" },
"updated_at": { "type": "string", "format": "date-time" }
}
},
"Identifiable": {
"type": "object",
"required": ["id"],
"properties": { "id": { "type": "integer", "minimum": 1 } }
}
},
"type": "object",
"allOf": [
{ "$ref": "#/$defs/Identifiable" },
{ "$ref": "#/$defs/Timestamps" },
{
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
}
]
}
Damit hat das Objekt id, created_at, updated_at UND name. allOf garantiert dass alle Eigenschaften erfüllt sind.
anyOf - toleranteste Komposition
Mindestens ein Sub-Schema muss passen. Mehrfach-Match ist OK.
{
"anyOf": [
{ "type": "string", "format": "email" },
{ "type": "string", "format": "uri" }
]
}
Damit ist sowohl "max@example.com" als auch "https://example.com" akzeptiert. Aber auch "max@example.com" das zufällig wie eine URI aussieht - Mehrfach-Match ist toleriert.
oneOf - exklusive Union
GENAU ein Sub-Schema muss passen. Mehrfach-Match → Fehler.
{
"oneOf": [
{
"type": "object",
"properties": { "type": { "const": "user" }, "email": { "type": "string" } },
"required": ["type", "email"]
},
{
"type": "object",
"properties": { "type": { "const": "guest" }, "sessionId": { "type": "string" } },
"required": ["type", "sessionId"]
}
]
}
Das ist das Tagged-Union-Pattern. Ein Diskriminator-Feld (hier "type") sorgt dafür dass nur ein Branch matchen kann.
anyOf vs. oneOf in der Praxis
Faustregel: wenn die Varianten überlappen könnten, anyOf. Wenn sie sich gegenseitig ausschließen sollen, oneOf mit Discriminator.
Vermeide: oneOf ohne Discriminator. Es ist langsamer (Validator muss alle Branches probieren) und Fehlermeldungen sind schlecht ("did not match exactly one schema").
Performance-Aspekt: discriminator
In OpenAPI 3.x gibt es ein discriminator-Keyword das pure JSON Schema nicht hat. Es zeigt dem Validator welches Feld als Selektor genutzt wird:
# OpenAPI 3.1
oneOf:
- $ref: '#/components/schemas/User'
- $ref: '#/components/schemas/Guest'
discriminator:
propertyName: type
mapping:
user: '#/components/schemas/User'
guest: '#/components/schemas/Guest'
Damit muss der Validator nur das eine "richtige" Schema testen - bei großen Discriminator-Unions massiv schneller.
not - der Außenseiter
not negiert ein Schema. Selten direkt nützlich.
{
"type": "string",
"not": { "enum": ["admin", "root"] }
}
Bedeutung: ein String, der nicht in der enum-Liste steht. Funktioniert, ist aber meist klarer mit eigenem Schema oder allOf + custom keyword.
Komposition mit additionalProperties
Vorsicht: allOf + additionalProperties: false ist tricky. Das additionalProperties: false in einem Sub-Schema gilt nur für die in DIESEM Sub-Schema deklarierten properties. Das kann zu false-positives führen.
Workaround in Draft 2019-09+: unevaluatedProperties verhält sich kombinations-aware.
Im Tool testen
Probiere ein Schema mit oneOf und füttere zwei Payloads ein - eins das nur einem Branch entspricht, eins das beiden entspricht. Du siehst den oneOf-Fehler beim zweiten klar.