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.

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:
requiredverlangt nur, dass der Schlüssel existiert:{"name": ""}ist gültig. Um einen leeren String abzulehnen, müssen SieminLength: 1ergänzen;additionalProperties: falsevergröß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;propertiesbeschrä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" }
}
}
$defsist die in 2019-09 eingeführte Form; draft-07 verwendetdefinitions;- 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
$refmit Schwester-Schlüsselwörtern variiert je nach Version). Brauchen Sie Komposition, verwenden SieallOf.
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:
compilenur einmal aufrufen, auf oberster Modulebene; erneutes Kompilieren bei jeder Anfrage macht aus einer 10-ms-Validierung Hunderte von Millisekunden;strict: truemacht falsch geschriebene Schlüsselwörter früh sichtbar (etwaminlengthstattminLength), es lohnt sich also, es zu aktivieren;- Nur
ajv-formatslässtformat: "email"undformat: "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; uniqueItemsfü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: falsemitallOf, werden im Subschema deklarierte Felder vom Elternteil als „zusätzlich“ beurteilt, Sie müssen sie also in einem einzigen Schema zusammenführen oderunevaluatedPropertiesverwenden.
8. Checkliste
- [ ] 2020-12 festlegen und
$schemaam Dateianfang deklarieren; - [ ] Gemeinsame Strukturen mit
$defs+$refherausziehen, statt zu kopieren und einzufügen; - [ ] Diskriminatoren statt
oneOfbevorzugen; - [ ] Das
compile-Ergebnis des Validators wiederverwenden und den Strict Mode aktivieren; - [ ] Klar dokumentieren, ob
formaterzwungen 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.