JSON Toolkit

JSON-Schema-Vertragstests: Validierungsregeln in die CI schweißen

Fazit: Ein Schema hört beim Schreiben nicht auf — dort beginnt der Verfall. Ein Tippfehler lockert die Validierung still, ein Refactoring bricht alle Fixtures. Dieser Leitfaden zeigt eine praktikable Vertragstest-Architektur: positive, negative und Snapshot-Fixtures, Lint des Schemas selbst, einmalige kompilierte Wiederverwendung und vier CI-Tore, die einen Vertragsbruch in einen roten Build verwandeln.

Mittel~8 Min. LesezeitVeröffentlicht 2026-11-05Aktualisiert 2026-11-05
Diagramm zu JSON-Schema-Vertragstests und CI-Validierungstoren

Fazit: Ein Schema ist Code, und Code braucht Tests. Positive Fixtures beweisen, dass er das Richtige durchlässt; negative beweisen, dass er das Falsche zurückweist; Snapshots überwachen unbeabsichtigte Änderungen. Schließlich halten ajv compile und Snapshots mit --ci die Nachtwache in Ihrer Pipeline.

In dem Moment, in dem eine Validierungsregel bricht, wirft nichts einen Fehler: Eine Verbreiterung der Akzeptanz bleibt unbemerkt, eine Verschärfung der Ablehnung zahlt das ganze Team. Ein Schema ist keine Konfigurationsdatei — es ist Code, den niemand testet.

1. Das Schema ist kaputt — wer erfährt es zuerst

Beginnen wir mit einer Falle, in die wir wirklich getreten sind. Deklarieren Sie ein Schema nach dem 2020-12-Entwurf und kompilieren Sie es mit dem Standardpaket von Ajv, fliegt sofort eine Ausnahme:

Error: no schema with key or ref "https://json-schema.org/draft/2020-12/schema"

Der Standardeinstiegspunkt von Ajv 8 kennt nur draft-07; für 2020-12 braucht es den Import aus ajv/dist/2020. Eine weitere Falle: Im Strict-Modus wirft format: "email" ohne installiertes ajv-formats ebenfalls beim Kompilieren eine Ausnahme:

Error: unknown format "email" ignored in schema at path "#/properties/email"

Diese Fehler lokal bei der ersten Kompilation zu sehen, ist gut. Das Problem: Viele Projekte schieben die Kompilation auf den Zeitpunkt des eingehenden Requests, und das kaputte Schema wartet auf sein erstes Opfer. Noch heimtückischer ist das halbkaputte Schema: ein required, versehentlich als String statt als Array geschrieben, wirft beim Kompilieren schema is invalid: data/required must be array — aber lief dieser Schritt nie in der CI, liegt es in der Produktion begraben.

Das erste Tor prüft also nicht die Daten, sondern das Schema selbst. Ein Befehl:

node scripts/lint-schemas.mjs
// scripts/lint-schemas.mjs
import { readFileSync, readdirSync } from 'node:fs';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';

const ajv = new Ajv2020({ allErrors: true, strict: true });
addFormats(ajv);
for (const file of readdirSync('schemas')) {
  const schema = JSON.parse(readFileSync(`schemas/${file}`, 'utf8'));
  ajv.compile(schema);
  console.log(`ok ${file}`);
}

Warum nicht npx ajv compile: Das Paket ajv v8 enthält keine CLI, npx ajv scheitert mit could not determine executable to run; die CLI steckt im separaten Paket ajv-cli (bei uns 5.0.0). ajv-cli 5 unterstützt aber nur draft-07 und draft-2019-09 — auf das 2020-12-Schema aus Abschnitt 1 angewandt wirft es denselben Fehler no schema with key or ref "https://json-schema.org/draft/2020-12/schema". Ein 2020-12-Projekt lintet daher über denselben Ajv2020-Einstieg wie im Artikel; npx ajv-cli compile -s '**/*.schema.json' --strict passt nur zu draft-07-Projekten.

Fahren Sie ihn wie einen Linter: Tippfehler, veraltete Schlüsselwörter und nicht registrierte Formate erscheinen schon im PR. Das ist die erste Gemeinsamkeit von Schema-Tests und gewöhnlichem Code-Testen: zuerst prüfen, dass er kompiliert.

2. Das Trio: positiv, negativ, Snapshot

Das Skelett des Schema-Tests besteht aus drei Teilen.

Positive Fixtures: aus echten Antworten gezogene Samples, die durchgehen müssen. Nehmen Sie sie von der Staging-Umgebung, nicht aus dem Gedächtnis — handgeschriebene Positive decken nur den Happy Path ab und übersehen null, leere Strings und Grenzen mit nur Pflichtfeldern.

Negative Fixtures: für jede Einschränkung des Schemas mindestens eine verletzende Eingabe. Für minimum: 0 das Paar -1 (Ablehnung) und 0 (Durchlass); für enum ein Wert außerhalb der Liste; für additionalProperties: false ein überflüssiges Feld. Negative sind wichtiger als positive: Ein Schema, das still schlechte Daten durchlässt, ist ein stiller Bug, der bis in die Datenbank verschmutzt.

Die Namenskonvention, die den Dateinamen zur Assertion macht, lässt sich aus Open Source kopieren:

schemas/order.schema.json
fixtures/order/valid-minimal.json        # muss durchgehen
fixtures/order/valid-full.json           # muss durchgehen
fixtures/order/invalid-type.json         # muss abgelehnt werden
fixtures/order/invalid-extra-field.json  # muss abgelehnt werden

Das in Node eingebaute node:test genügt:

import { test } from 'node:test';
import { readFileSync, readdirSync } from 'node:fs';
import assert from 'node:assert/strict';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';

const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);
const validate = ajv.compile(
  JSON.parse(readFileSync('schemas/order.schema.json', 'utf8'))
);

for (const f of readdirSync('fixtures/order')) {
  const data = JSON.parse(readFileSync(`fixtures/order/${f}`, 'utf8'));
  if (f.startsWith('valid-')) {
    test(`${f} sollte durchgehen`, () => { assert.ok(validate(data), JSON.stringify(validate.errors)); });
  } else {
    test(`${f} sollte abgelehnt werden`, () => { assert.ok(!validate(data)); });
  }
}

Snapshots: Serialisieren Sie validate.errors in eine Datei und vergleichen Sie. Jemand ändert unbeabsichtigt eine Einschränkung — der Snapshot rötet sich sofort. In Jest/Vitest ist es eine Zeile: expect(errors).toMatchSnapshot(); in der CI immer --ci mitgeben: Neue Snapshots schlagen dann fehl, statt still erzeugt zu werden, und Drift wird nicht stillschweigend akzeptiert.

3. Alle Fehler in einem Durchlauf sammeln

Eine oft übersehene Einstellung beim Testen von Negativen: Standardmäßig stoppt der Validator beim ersten Fehler. Die Daten sind an drei Stellen kaputt, Sie sehen eine, fixen, starten neu — und so weiter. Mit allErrors: true (Ajv) oder iter_errors (Python) sehen Sie alles auf einmal:

from jsonschema import Draft202012Validator

validator = Draft202012Validator(schema)
errors = sorted(validator.iter_errors(bad), key=lambda e: list(e.absolute_path))
for e in errors:
    print(list(e.absolute_path), e.validator, e.message)

Python hat auch einen kontra-intuitiven Fakt: Welche Meldung validate() ausgibt, wählt nicht „die erste“, sondern best_match. In unserer Messung bei drei Verletzungen (falscher Typ von id, role außerhalb der Liste, überflüssiges Feld extra) wurde nur additionalProperties gemeldet — der Typfehler von id blieb verborgen. Verwenden Sie in der CI immer iter_errors für das volle Bild.

Und eine noch leichtere Falle: format ist standardmäßig keine Assertion. Der Entwurf 2020-12 stuft format als Annotation ein: Das jsonschema von Python prüft ihn nicht ohne übergebenen format_checker — in unserem Test lief "not-an-email" frei durch. Ajv verlangt die Installation und Registrierung von ajv-formats. Wenn ein Format wichtig ist (E-Mail, Datum, UUID), muss es eine negative Fixture geben, die beweist, dass Ihre Validator-Konfiguration ihn wirklich prüft.

4. Einmal kompilieren — den Compiler nicht als Wegwerfartikel benutzen

Lokale Messungen (Node v22.13.1, Ajv 8.17, dasselbe dreifach verschachtelte Schema, dieselben verletzenden Daten):

Ansatz Zeit für 10.000 Validierungen
Jedes Mal new Ajv() und compile 42233 ms
Instanz auf Modulebene + compile einmal 6.9 ms

Ein Faktor 6000. Ein Validator im Request-Handler, der bei jedem Aufruf ein neues Ajv erzeugt, ist der häufigste Performance-Unfall. Halten wir auch einen weitverbreiteten Irrtum fest: „ajv.validate(schema, data) kompiliert jedes Mal neu“. Gemessen: 100.000 Aufrufe in 8.2 ms — vergleichbar mit 11.2 ms der einmal kompilierten Funktion, denn Ajv hält einen internen Schema-Cache. Die echte Not ist kein Cache-Miss, sondern die nicht wiederverwendete Instanz. In Python gilt dasselbe: Ein wiederverwendeter Draft202012Validator ist rund 5000-mal schneller als jedesmaliger validate()-Aufruf (2000 Iterationen: 0.3 ms gegen 1566 ms), denn letzterer prüft bei jedem Aufruf das Schema selbst.

5. Die Tore in die CI schweißen: vier Türen

Von billig nach teuer geordnet: Fallen die vorderen, müssen die hinteren nicht laufen:

Tor Befehlsform Was es erwischt Kosten
Schema-Lint node scripts/lint-schemas.mjs Syntaxfehler, veraltete Schlüsselwörter Millisekunden
Fixture-Prüfung Die Testsuite aus Abschnitt 2 Entkopplung von Fixtures und Schema, versehentlich geänderte Einschränkungen Millisekunden
Diff brechender Änderungen json-schema-diff-validator gegen main Entferntes required, verengtes enum, verschärftes pattern Sekunden
Prüfung echter Antworten Smoke-Tests gegen Staging mit Assertion .toMatchSchema() Drift des echten Endpunkts Sekunden

Minimale GitHub-Actions-Konfiguration:

- run: npm ci
- run: npm run lint:schemas        # Ajv2020 lint
- run: npm run test:schema         # Fixture-Trio
- run: npx jest --ci --testPathPattern=schema   # Snapshot-Driftwache

Das Tor für brechende Änderungen verdient eigene Betonung: Werkzeuge wie json-schema-diff-validator markieren „required-Feld entfernt“ und „enum-Wert verloren“ schon im Review — genau die beiden Änderungen, die gewöhnliche API-Diffs gern als „nicht brechend“ einstufen. Ein entferntes Antwortfeld ist für tolerante Konsumenten harmlos und für Konsumenten mit exhaustivem Switch ein Produktionsabsturz.

6. Das Fehlerobjekt asserten, nicht nur den Booleschen Wert

Ein negativer Test mit nur validate(data) === false genügt nicht. Beim selben Schema sollten Sie sich vergewissern, dass es aus dem richtigen Grund ablehnt: Eine fremde Einschränkung kann zufällig durchgelaufen sein. Das Fehlerobjekt von Ajv hat eine berühmte Asymmetrie: Schlägt required fehl, zeigt instancePath auf das Elternobjekt, und der Name des fehlenden Felds versteckt sich in params.missingProperty. Gemessen bei fehlendem id:

{ "instancePath": "", "schemaPath": "#/required",
  "keyword": "required", "params": { "missingProperty": "id" } }

Die Assertion lautet „es existiert ein Fehler mit keyword=required und missingProperty=id“, nicht ein vages errors.length > 0. Die vollständige Analyse der Fehlerfeld-Semantik steht im Artikel JSON-Schema-Fehler unverständlich: Ajv-Fehler lokalisieren und Meldungen gestalten.

7. Häufige Fallen — Schnelltabelle

Falle Folge Maßnahme
Nur 200 Happy Paths getestet Drift der Fehlerhüllen bleibt unbemerkt Schema und Fixtures auch für Fehlerantworten
Generierte Schemas ohne Review Aus einer Probe abgeleitete required-Felder werfen echte Daten raus; das Team schaltet die Validierung ab Menschliche Prüfung von required/enum nach der Generierung
Ein globales Schema für alle Versionen v1/v2-Unterschiede verwischen Schemas zusammen mit Endpunkten versionieren
Volle Payloads bei Fehlern in Logs PII und Secrets lecken in CI-Logs Nur JSON-Pointer-Pfade und gekürzte Schnipsel schreiben
Einfrorene Enums Produktumbenennung (canceled→cancelled) bricht Validierung Enum-Änderungen über Kompatibilitätsreview
Nicht gepinnte Validatoreversionen Unterschiedliches Verhalten je Maschine Validatoreversionen in der CI pinnen
Ersatz für Autorisierungstests Ein 403 passt zum Fehlerschema, die Rollenlogik ist trotzdem falsch Schema prüft Struktur; Autorisierung separat testen
Semantik von format vergessen Müll-Adressen laufen frei durch ajv-formats installieren / format_checker übergeben, Negative ergänzen

8. Einführungs-Checkliste

  1. Schemadateien in die Versionsverwaltung, node scripts/lint-schemas.mjs in den Lint;
  2. Pro Schema mindestens 1 positive Fixture und je Einschränkung 1 negative; der Dateiname ist die Assertion;
  3. Fehlerobjekte als Snapshots; in der CI --ci mitgeben, damit Snapshots nicht selbst driften;
  4. Validatoreinstanzen auf Modulebene wiederverwenden — nie new in Handlern;
  5. Diff brechender Änderungen an die PR hängen; required/enum-Änderungen erzwingen Review;
  6. Der erste PR bewusst: das Tor muss scheitern können — required entfernen, das Rot sehen, zurücksetzen.

Die Frage der Strenge der Validierung (wann man additionalProperties schließt) ist ein anderes Thema — siehe additionalProperties offen gelassen: Schema-Validierung ist wertlos. Dieser Artikel handelt davon, geschriebene Regeln dauerhaft verlässlich zu halten. Für die Grundlagen: JSON Schema von den Grundlagen zur Praxis.

FAQ

Wie hängen Schema-Validierung und vertragsbasiertes Testen à la Pact zusammen?

Das ist Arbeitsteilung. Die Schema-Validierung beantwortet „ist dieses Dokument strukturell korrekt“ — eine einseitige Assertion. Konsumentgetriebenes Vertragstesten (Pact) beantwortet „erfüllt der Provider noch die konkreten Bedürfnisse des Konsumenten X“ und verlangt eine zweiseitige Prüfung. Kleine Teams starten mit Schema-Toren und führen Pact ein, wenn die Zahl der Dienste wächst.

Wie viele negative Tests reichen?

Ein Negativ pro Einschränkung ist die Untergrenze: so viele negative Fixtures wie Einschränkungen (required, type, minimum, enum, pattern, additionalProperties…), dazu Paare aus abgelehntem und akzeptiertem Grenzwert. Weniger Negative als Einschränkungen heißt: eine Einschränkung bleibt unbewacht.

Welche Schema-Änderung gilt als „brechend“?

Urteilen Sie aus Konsumentensicht: Ein Antwortfeld entfernen, ein enum entfernen oder verengen, ein pattern verschärfen, ein Pflichtanfragefeld hinzufügen — all das bricht; ein optionales Antwortfeld hinzuzufügen ist meist sicher — vorausgesetzt, der Dekoder des Konsumenten toleriert unbekannte Schlüssel. Automatisieren Sie den Vergleich mit json-schema-diff-validator zum PR-Zeitpunkt, Grauzonen entscheiden Sie von Hand.

Gegen die echte API oder gegen Mocks testen?

Beides, aber geschichtet: Fixture-Tests berühren kein Netz, laufen in Millisekunden und bei jedem PR; die echte Prüfung schlägt auf die Staging-Umgebung, dauert Sekunden und gehört zur Smoke-Pipeline, wird ohne Zugangsdaten per skipif übersprungen. Teams, die in der CI nur die echte API fahren, zerschleiern sich an Netzwerkflackern und Rate-Limits und schalten die Tests schließlich ab.

Quellen

Verwandte Artikel

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

JSON Schema: von den Grundlagen bis zur Produktion

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. LesezeitAktualisiert 2026-10-30