json-validator.de

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.

Foto von Jan-Tristan Rudat

Von Jan-Tristan Rudat

Redakteur json-validator.de

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.

KeywordMatch-AnforderungTypischer Use-Case
allOfAlle Sub-Schemas müssen passenMixin-Pattern, Schema-Erweiterung
anyOfMindestens eins muss passenOffene Union-Typen
oneOfGenau eins muss passenTagged 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.

Mehr zum Thema