JSON Toolkit

JSON Schema: von den Grundlagen bis zur Produktion

Fazit: Es gibt viele Schlüsselwörter, aber nur ein Dutzend wird täglich verwendet. Bringen Sie type/properties/required/$ref richtig hin, behandeln Sie Fehlermeldungen gut, und Sie decken achtzig Prozent der realen Fälle ab.

Fortgeschritten~14 Min. LesezeitVeröffentlicht 2026-10-30Aktualisiert 2026-10-30
Titelbild für JSON Schema: von den Grundlagen bis zur Produktion

Kurz gesagt: Legen Sie $schema zuerst auf 2020-12 fest und ziehen Sie dann gemeinsame Strukturen mit $defs + $ref heraus; verwenden Sie oneOf nur, wenn genau ein Zweig passen muss, und bevorzugen Sie überall sonst Diskriminatoren oder anyOf.

Ein Schema ist kein Anhang zur Dokumentation; es ist ein Vermögenswert, der ausgeführt, getestet und generiert werden kann.

1. Zuerst die Version klären

Version $schema-Wert Status Hinweise
draft-04 http://json-schema.org/draft-04/schema# Veraltet Altprojekte und manche OpenAPI-2.0-Werkzeuge
draft-07 http://json-schema.org/draft-07/schema# Weit verbreitet Die De-facto-Basis von OpenAPI 3.0
2019-09 https://json-schema.org/draft/2019-09/schema Verfügbar Führte $defs und das Annotationsvokabular ein
2020-12 https://json-schema.org/draft/2020-12/schema Aktuell empfohlen Führte prefixItems ein und änderte die Semantik von items

Verwenden Sie 2020-12 für neue Projekte. Beachten Sie, dass in 2020-12 items nur die „übrigen Elemente“ beschränkt – tupelartige Deklarationen wanderten zu prefixItems. Das ist die häufigste Falle bei der Migration von draft-07.

2. Ein minimal brauchbares Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/user.json",
  "type": "object",
  "required": ["id", "name"],
  "properties": {
    "id": { "type": "string", "minLength": 1 },
    "name": { "type": "string", "minLength": 1, "maxLength": 64 },
    "age": { "type": "integer", "minimum": 0, "maximum": 150 },
    "tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
  },
  "additionalProperties": false
}

Dieses Schema deckt die vier häufigsten Anliegen ab: Typbeschränkungen, Pflichtfelder, Wertebereiche und Array-Elementbeschränkungen.

3. Schlüsselwortkarte

3.1 Allgemein

Schlüsselwort Zweck Worauf achten
type Der Typ; darf ein Array sein "type": ["string", "null"] bedeutet nullable
enum Aufgezählte Werte Werte müssen eindeutig sein
const Einschränkung auf einen einzigen Wert Gut für feste Werte wie eine Versionsnummer
$ref Referenziert ein anderes Schema Referenziert das ganze Schema und verschmilzt nicht mit Schwester-Schlüsselwörtern des referenzierenden Objekts

3.2 Zahlen

multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum. Beachten Sie, dass exclusiveMaximum in draft-04 ein Boolean war (zusammen mit maximum verwendet); erst ab draft-07 wurde es ein eigenständiges numerisches Schlüsselwort – was viele „aus alten Dokumenten kopierte Schemas“ im Strict Mode scheitern lässt.

3.3 Strings

minLength, maxLength, pattern (ein ECMA-262-Regular-Expression) und format (eine Annotation).

minLength zählt Unicode-Codepoints, nicht UTF-16-Einheiten und nicht Bytes. Die Validierung mit JavaScripts string.length wird dazu abweichen.

3.4 Arrays

Schlüsselwort Zweck
prefixItems Die tupelartige Beschränkung von 2020-12, passt Elemente nach Index einzeln zu
items Eine einheitliche Beschränkung für die übrigen Elemente (2020-12)
contains + minContains/maxContains Mindestens/höchstens wie viele passende Elemente vorhanden sein müssen
uniqueItems Elemente müssen alle verschieden sein (Tiefenvergleich; bei großen Arrays meiden)
minItems / maxItems Längenbeschränkungen

3.5 Objekte

properties, required, patternProperties, additionalProperties, propertyNames, minProperties, maxProperties, dependentRequired, dependentSchemas.

Drei häufige Missverständnisse:

  1. required verlangt nur, dass der Schlüssel existiert: {"name": ""} ist gültig. Um einen leeren String abzulehnen, müssen Sie minLength: 1 ergänzen;
  2. additionalProperties: false vergrößert Kompatibilitätsprobleme: Ein zusätzliches Feld von einem Client lässt die ganze Payload scheitern, die Weiterentwicklung der API zwingt also jeden Aufrufer zu einem gleichzeitigen Upgrade;
  3. properties beschränkt nicht deklarierte Schlüssel nicht – standardmäßig sind sie erlaubt.

3.6 Logische Komposition

{
  "oneOf": [
    { "type": "object", "required": ["cardNumber"], "properties": { "cardNumber": { "type": "string" } } },
    { "type": "object", "required": ["accountId"], "properties": { "accountId": { "type": "string" } } }
  ]
}
Schlüsselwort Semantik Typisches Scheitern
allOf Alle müssen erfüllt sein Mehrere Zweige erlegen demselben Feld widersprüchliche Beschränkungen auf
anyOf Mindestens einer muss erfüllt sein Die Fehlermeldung lässt sich schwer einem bestimmten Zweig zuordnen
oneOf Genau einer muss erfüllt sein Daten passen auf zwei Zweige und scheitern, obwohl sie „offensichtlich korrekt aussehen“
not Darf nicht erfüllt sein Selten verwendet, und die am wenigsten freundlichen Fehlermeldungen

oneOf ist das Schlüsselwort, das am ehesten „gültige Daten, die die Validierung nicht bestehen“ erzeugt. Überschneiden sich die Zweigbeschränkungen (etwa beide erlauben zusätzliche Felder), passt ein Datensatz auf mehrere Zweige, und oneOf scheitert glatt. Wechseln Sie in dem Fall zu anyOf oder verwenden Sie ein Diskriminatorfeld mit const in den Zweigen.

{
  "type": "object",
  "required": ["kind"],
  "properties": { "kind": { "enum": ["text", "image"] } },
  "if": { "properties": { "kind": { "const": "image" } } },
  "then": { "required": ["url", "width", "height"] },
  "else": { "required": ["content"] }
}

Der Diskriminatorstil ist lesbarer als oneOf und liefert bessere Fehler.

4. Aufteilen und Wiederverwenden

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://bbdrug.com/schemas/order.json",
  "$defs": {
    "money": { "type": "string", "pattern": "^-?\\d+\\.\\d{2}$" },
    "address": {
      "type": "object",
      "required": ["city"],
      "properties": { "city": { "type": "string" }, "detail": { "type": "string" } }
    }
  },
  "type": "object",
  "required": ["total", "shipTo"],
  "properties": {
    "total": { "$ref": "#/$defs/money" },
    "shipTo": { "$ref": "#/$defs/address" }
  }
}
  • $defs ist die in 2019-09 eingeführte Form; draft-07 verwendet definitions;
  • Dateiübergreifende Referenzen verwenden relative $ids, aber verlassen Sie sich in der Produktion nicht auf das Abrufen entfernter Schemas zur Laufzeit – bündeln Sie sie stattdessen als statische Dateien;
  • Neben einer Referenz können Sie keine Beschränkungen ergänzen (das Verhalten von $ref mit Schwester-Schlüsselwörtern variiert je nach Version). Brauchen Sie Komposition, verwenden Sie allOf.

5. Laufzeitvalidierung mit Ajv

import Ajv2020 from 'ajv/dist/2020';
import addFormats from 'ajv-formats';
import orderSchema from './order.json' with { type: 'json' };

const ajv = new Ajv2020({
  allErrors: true,
  strict: true,
  allowUnionTypes: true,
});
addFormats(ajv);

const validateOrder = ajv.compile(orderSchema);

export function assertOrder(input: unknown): void {
  if (validateOrder(input)) {
    return;
  }
  const detail = (validateOrder.errors ?? []).map((errorItem) => ({
    path: errorItem.instancePath || '/',
    keyword: errorItem.keyword,
    message: errorItem.message,
    params: errorItem.params,
  }));
  throw new Error(`Order data failed validation: ${JSON.stringify(detail)}`);
}

Kernpunkte:

  • compile nur einmal aufrufen, auf oberster Modulebene; erneutes Kompilieren bei jeder Anfrage macht aus einer 10-ms-Validierung Hunderte von Millisekunden;
  • strict: true macht falsch geschriebene Schlüsselwörter früh sichtbar (etwa minlength statt minLength), es lohnt sich also, es zu aktivieren;
  • Nur ajv-formats lässt format: "email" und format: "date-time" tatsächlich wirken;
  • 2020-12 erfordert den Einstiegspunkt ajv/dist/2020 – verwenden Sie nicht den Standardexport.

6. Wie man Fehlermeldungen entwirft

Der Validator liefert „welches Schlüsselwort gescheitert ist“; der Nutzer will „welches Eingabefeld falsch ist“. Es lohnt sich, eine Abbildungsschicht hinzuzufügen:

const FIELD_LABEL: Record<string, string> = {
  '/name': 'Name',
  '/age': 'Age',
  '/tags': 'Tags',
};

/**
 * Convert Ajv errors into display-ready messages.
 * @param errors The list of Ajv errors
 * @returns An array of display messages
 */
function toMessages(errors: Array<{ instancePath: string; keyword: string; params: unknown }>): string[] {
  return errors.map((errorItem) => {
    const label = FIELD_LABEL[errorItem.instancePath] ?? errorItem.instancePath;
    if (errorItem.keyword === 'required') {
      const missing = (errorItem.params as { missingProperty: string }).missingProperty;
      return `${label} is missing the required field ${missing}`;
    }
    if (errorItem.keyword === 'minLength') {
      return `${label} is too short`;
    }
    return `${label} failed validation (${errorItem.keyword})`;
  });
}

Auf der API-Seite eine maschinenlesbare Struktur beibehalten, damit das Frontend entscheiden kann, wie es sie darstellt:

{
  "type": "https://bbdrug.com/problems/validation",
  "title": "Request data validation failed",
  "status": 422,
  "errors": [
    { "path": "/items/0/price", "keyword": "pattern", "message": "Must be a monetary string with two decimal places" }
  ]
}

7. Performance und Fallen

  • Die Validierungskosten wachsen mit Schema-Komplexität und Datenmenge; mehrere oneOf-Zweige verursachen kombinatorische Explosion, die bei Tausenden Datensätzen sehr auffällt;
  • uniqueItems führt einen O(n²)-Tiefenvergleich über das Array aus; bei großen Arrays deduplizieren Sie stattdessen in der Geschäftsschicht;
  • Vermeiden Sie katastrophales Backtracking in pattern (etwa (a+)+$); das ist der klassische Einstiegspunkt für ReDoS;
  • Kombiniert man additionalProperties: false mit allOf, werden im Subschema deklarierte Felder vom Elternteil als „zusätzlich“ beurteilt, Sie müssen sie also in einem einzigen Schema zusammenführen oder unevaluatedProperties verwenden.

8. Checkliste

  • [ ] 2020-12 festlegen und $schema am Dateianfang deklarieren;
  • [ ] Gemeinsame Strukturen mit $defs + $ref herausziehen, statt zu kopieren und einzufügen;
  • [ ] Diskriminatoren statt oneOf bevorzugen;
  • [ ] Das compile-Ergebnis des Validators wiederverwenden und den Strict Mode aktivieren;
  • [ ] Klar dokumentieren, ob format erzwungen wird;
  • [ ] Fehlerantworten liefern den Pfad, das Schlüsselwort und eine nutzerfreundliche Meldung;
  • [ ] Das Schema in CI einbinden: die Fälle der JSON Schema Test Suite als Regressionstests ausführen.

FAQ

Wie wähle ich einen JSON-Schema-Draft?

Verwenden Sie 2020-12 für neue Projekte und deklarieren Sie $schema am Dateianfang. Geht das Schema in OpenAPI 3.1, muss es ebenfalls 2020-12 sein, weil 3.1 diese Version direkt einbettet; behalten Sie draft-07 nur für Projekte, die noch an ältere Toolchains gebunden sind. Beide verhalten sich bei items und bei $ref-Schwester-Schlüsselwörtern unterschiedlich, mischen Sie sie also nicht in einem Schemaset.

Warum vergrößert additionalProperties: false Kompatibilitätsprobleme?

Es behandelt jeden nicht deklarierten Schlüssel als Fehler, ein zusätzliches Feld von einem Client lässt also die ganze Payload scheitern. Jeder Aufrufer muss dann bei jeder Weiterentwicklung der API gleichzeitig upgraden, sonst werden alte Clients abgelehnt. Schemas im Konfigurationsstil sollten zusätzliche Felder ausdrücklich schließen, während öffentliche APIs die Kompatibilitätskosten abwägen müssen.

Sollte ich oneOf oder anyOf verwenden?

Verwenden Sie oneOf nur, wenn die Daten auf genau einen Zweig passen müssen. Können die Zweigbeschränkungen gleichzeitig gelten, passt ein Datensatz auf mehrere Zweige, und oneOf scheitert glatt; wechseln Sie zu anyOf oder ergänzen Sie einen const-Diskriminator in jedem Zweig.

Quellen

Verwandte Artikel

Zweiweg-Synchronisierung von Schema und TypeScript-Typen

Typen und Schemas decken je ein Segment ab: eines zur Kompilierzeit, eines zur Laufzeit. Die Frage ist nicht, welches man wählt, sondern wie man beide automatisch synchron hält.

Fortgeschritten~11 Min. LesezeitAktualisiert 2026-10-30

Kernpunkte für den Entwurf von JSON-API-Datenverträgen

Sechs Dinge lassen sich einmal ausgeliefert sehr schwer ändern: Response-Hüllen, Fehlerformate, Paginierungsform, Darstellung von IDs und Zeit, die Semantik von null versus fehlend und die Versionsentwicklung.

Fortgeschritten~13 Min. LesezeitAktualisiert 2026-10-30

Ohne additionalProperties validiert das Schema nichts

Eine bestandene Validierung bedeutet keine sauberen Daten. Pydantic ignoriert unbekannte Felder standardmäßig, Zod entfernt sie, Ajv lässt sie durch — erfundene Parameter und Tippfehler-Schlüssel rutschen lautlos durch. Dieser Artikel zeigt die drei Formen stiller Akzeptanz und liefert vollständige Rezepte: Objekte schließen, Typos-Konvertierung kontrollieren, jede Verschachtelungsebene festziehen.

Intermediate~8 Min. LesezeitAktualisiert 2026-11-02