JSON Toolkit

JSON-Verträge versionieren: Kompatibilität, Veraltung und Datenmigration

Fazit: Ein Feld hinzuzufügen ist nicht automatisch sicher. Behandelt Kompatibilitätsgrenzen, Versionsfelder, Veraltungsfenster, Doppelschreib-Migration, Vertragstests und Canary-Metriken.

Experte~10 Min. LesezeitVeröffentlicht 2026-10-31Aktualisiert 2026-10-31
Titelbild für die Versionierung von JSON-Verträgen, Kompatibilität und Datenmigration

Kurz gesagt: Führen Sie Versionsentwicklung als expand, backfill, dann contract aus und persistieren Sie die Version im Dokument; bestätigen Sie, dass Konsumenten unbekannte Felder und Enum-Werte tolerieren, bevor Sie etwas ausliefern, und führen Sie Veraltung in der Reihenfolge freeze, announce, observe, remove durch.

Das Schwierige an Versionierung ist nicht, Code zu ändern; es ist, dass Sie nicht wissen können, wer das alte Feld noch liest. Kompatibilität ist nie ein einseitiges Versprechen – sie ist eine fortlaufende Abstimmung zwischen Herausgeber und unbekannten Konsumenten.

1. Kompatibilitätsgrenzen: additiv ist nicht automatisch sicher

Lehrbücher nennen ein neues optionales Feld „rückwärtskompatibel“, aber das gilt nur, wenn Konsumenten unbekannte Felder tolerieren. Ein Feld hinzuzufügen bricht Clients weiterhin in mindestens drei Situationen:

  • der Konsument validiert mit einem Schema, das additionalProperties: false setzt;
  • der Konsument durchläuft das Objekt in fester Reihenfolge oder liest Werte nach Position statt nach Schlüssel;
  • das neue Feld verwendet einen alten Namen, ändert aber seine Bedeutung, etwa status von „Bestellstatus“ zu „Fulfillment-Status“ zu erweitern.

Eine sichere Ergänzung braucht drei Vorbedingungen: Das Feld ist optional, seine Bedeutung ist unabhängig, und Konsumenten ignorieren unbekannte Schlüssel. Bestätigen Sie alle drei, bevor Sie auch nur über eine Versionsnummer sprechen.

{
  "schemaVersion": 2,
  "orderId": "ord-8842",
  "status": "paid",
  "refundDeadline": "2026-11-30T00:00:00+08:00",
  "channel": "wechat"
}

Hier sind refundDeadline und channel optionale v2-Ergänzungen, während orderId und status ihre ursprüngliche Semantik behalten. schemaVersion wird mit dem Dokument persistiert, statt nur in einem HTTP-Header zu leben.

2. Breaking Changes, die Sie nicht ausliefern können

Behandeln Sie die folgenden Änderungen als rote Linien, die eine neue Version oder eine Doppelschreib-Übergangszeit erfordern. Die Dokumentation zu aktualisieren rettet Sie nicht:

Änderung Warum sie die Kompatibilität bricht Akzeptable Alternative
Ein Feld entfernen Konsumenten lesen sofort undefined Veralten lassen, beobachten, dann nach dem Fenster entfernen
Den Typ eines Feldes ändern "1" wird 1; stark typisierte Clients scheitern glatt, lose typisierte oft an der Validierung Einen neuen Feldnamen ergänzen und während der Übergangszeit doppelt schreiben
Ein Pflichtfeld ergänzen Alte Aufrufer senden es nicht, die Validierung scheitert also immer Einen Standardwert liefern oder es nur in der neuen Version erforderlich machen
Die Menge der Enum-Werte ändern switch-Anweisungen der Konsumenten haben keinen Fallback-Zweig Clients zuerst unbekannte Werte akzeptieren lehren, dann das Enum erweitern
Die Bedeutung eines Feldes ändern Typen bleiben gültig, während sich das Verhalten ändert – der am schwersten zu debuggende Fall Wie ein neues Feld behandeln und die alte Semantik einfrieren
Die Verschachtelungstiefe ändern Jeder Zugriffspfad muss sich ändern Die alte Form behalten und flache Felder oder einen expand-Parameter ergänzen

Die Bedeutung zu ändern ist die gefährlichste Zeile: Die Schema-Validierung bleibt grün, die Überwachung meldet keine Fehler, und nur Geschäftsmetriken driften. Jede Bedeutungsänderung sollte als neues Feld ausgeliefert werden.

3. Wo die Version lebt: Dokument, URL, Medientyp

Die drei üblichen Platzierungen dienen unterschiedlichen Zwecken:

Ort Beispiel Gut für Einschränkung
Dokumentfeld "schemaVersion": 2 Datenbanken, Message Queues, Offline-Backups Die Transportschicht kann nicht danach routen
URL-Pfad /v2/orders/8842 Öffentliche APIs Koppelt Ressourcenadressen an Versionen und macht Migration teuer
Medientyp application/vnd.acme.order.v2+json Interne Dienste mit Content Negotiation Aufrufer müssen Accept korrekt setzen

Eine praktische Arbeitsteilung: auf der Leitung Media-Type-Negotiation verwenden und immer ein Versionsfeld im Dokument persistieren. Alte Nachrichten in einer Queue werden nicht aufsteigen, weil Sie eine URL geändert haben, und Backup-Dateien auch nicht. Nur eine schemaVersion im Dokument gibt Replay, Backfill und Audits ein Fundament.

GET /orders/ord-8842 HTTP/1.1
Host: api.bbdrug.com
Accept: application/vnd.acme.order.v2+json

Versionieren Sie die URL, lassen Sie den alten Pfad zumindest während des gesamten Veraltungsfensters die alte Form zurückgeben, statt ihn stillschweigend auf die neue Implementierung zu zeigen. Zu breiteren API-Vertragsentscheidungen siehe Kernpunkte für den Entwurf von JSON-API-Datenverträgen.

4. Ein Veraltungsfenster öffnen

Veraltung ist keine einzelne Ankündigung; sie ist ein Fenster mit einem Beginn, einer Beobachtungsperiode und einem Ende. Ein praktikabler Rhythmus:

  1. Einfrieren: aufhören, das alte Feld zu schreiben, es aber weiter lesen.
  2. Ankündigen: das Entfernungsdatum in Dokumentation, Changelog und einem Response-Header gleichzeitig markieren.
  3. Beobachten: Lesezugriffe auf das alte Feld pro Aufrufer zählen, bis es zwei aufeinanderfolgende Perioden bei null bleibt.
  4. Entfernen: erst nach dem Schließen des Fensters, wenn Lesezugriffe null sind und Regressionstests bestehen, das Feld und seinen Kompatibilitätscode löschen.

Der Sunset-Response-Header (RFC 8594) gibt Clients ein maschinenlesbares Entfernungsdatum. Deprecation ist noch ein IETF-Draft: verwendbar, aber nehmen Sie nicht an, dass jeder Client es versteht. Paaren Sie ihn mit einem Link-Header, der auf den Migrationsleitfaden zeigt:

Deprecation: true
Sunset: Wed, 31 Mar 2027 23:59:59 GMT
Link: <https://bbdrug.com/docs/deprecations/order-status>; rel="deprecation"; type="text/html"

Die Fensterlänge ist Teamkonvention. Ein übliches Minimum ist ein Release-Zyklus oder 90 Tage, je nachdem, was länger ist. Öffentliche APIs sollten mehr bekommen, interne Dienste weniger, aber nie weniger als die Zeit, die jeder Aufrufer zum Upgrade braucht.

5. Konsumenten finden, bevor Sie veralten lassen

Vor dem Veralten müssen Sie „wer liest das noch?“ beantworten können. Sonst ist das Fenster Selbsttäuschung. Ordnen Sie die Belege nach Verlässlichkeit:

  • Gateway-Logs: Zugriffe auf alte Felder nach appId, User-Agent und Client-Version aggregieren.
  • Verteilte Traces: das alte Feld in der Serialisierungsschicht taggen, sodass ein Trace direkt den aufrufenden Dienstnamen zeigt.
  • Queue-Consumer-Registry: die Liste der Consumer Groups ist verlässlicher als eine Codesuche.
  • Repository-Suche: rg deckt nur Ihren eigenen Code ab; externe Aufrufer brauchen weiterhin Logs.
rg --type ts --type js 'legacyStatus|statusCode' src/ --stats

Ein Suchtreffer belegt „jemand in diesem Repository liest es“, nicht „niemand sonst“. Exportieren Sie die Gateway-Logs Tag für Tag und bestätigen Sie, dass Lesezugriffe auf alte Felder bei null bleiben, bevor Sie etwas entfernen.

6. Datenmigration: expand, backfill, contract

Martin Fowlers Parallel Change beschreibt drei Schritte: expand (alte und neue Felder koexistieren und beide Versionen können gelesen werden), backfill (historische Datensätze in die neue Form bringen) und contract (das alte Feld und Kompatibilitätszweige entfernen). Kehren Sie die Reihenfolge nicht um, sonst haben Sie keinen Rollback-Pfad.

// Migration skeleton: cursor by primary key, conditional writes for idempotency
async function migrateBatch(collection, cursor, batchSize = 500) {
  const records = await collection
    .find({ _id: { $gt: cursor } })
    .limit(batchSize)
    .toArray();

  if (records.length === 0) {
    return { nextCursor: cursor, done: true };
  }

  const operations = records
    .filter((record) => record.schemaVersion < 2)
    .map((record) => ({
      updateOne: {
        filter: { _id: record._id, schemaVersion: record.schemaVersion },
        update: {
          $set: {
            schemaVersion: 2,
            status: record.legacyStatus ?? record.status,
            refundDeadline: record.refundDeadline ?? null,
          },
        },
      },
    }));

  if (operations.length > 0) {
    await collection.bulkWrite(operations);
  }

  return { nextCursor: records.at(-1)._id, done: false };
}

Details, die leicht übersehen werden:

  • Idempotenz: die alte Versionsnummer in den filter aufnehmen, damit ein erneuter Lauf nie einen bereits migrierten Datensatz neu schreibt.
  • Batching und Checkpoints: mit dem _id-Cursor vorrücken, einen Checkpoint speichern und nach einem Fehler von dort fortsetzen.
  • Probelauf: das Skript gegen eine Produktions-Momentaufnahme laufen lassen und Datensatzzahlen und Checksummen vorher und nachher vergleichen.
  • Rollback: das alte Feld behalten, bis das neue Feld die Validierung besteht; Lesepfade mit einem Flag umschalten und nie allein durch Ändern von schemaVersion zurückrollen.
  • Validierung: nach der Migration Geschäftsbedingungen ausführen (nicht negative Beträge, zulässige Zustandsübergänge), nicht nur eine JSON-Parse-Prüfung.

Konfigurationsdaten folgen demselben Prozess; siehe JSON für Konfiguration, Fixtures und lokale Daten verwalten für die Organisation dieser Dateien.

7. Vertragstests: Kompatibilität in CI bringen

Kompatibilität darf nicht vom Gedächtnis des Code-Reviews abhängen; sie braucht Tests. Zwei Schichten genügen:

  1. Formtests: Antworten mit JSON Schema validieren, aber kein additionalProperties: false auf Schemas setzen, die Konsumenten verwenden.
  2. Consumer-Subset-Tests: die Felder, auf die sich der alte Konsument wirklich verlässt, in ein Sub-Schema herausziehen und prüfen, dass neue Antworten es weiterhin erfüllen.
// Contract test: new responses must still satisfy the old consumer subset
import Ajv from 'ajv';
import assert from 'node:assert/strict';
import { test } from 'node:test';

const ajv = new Ajv({ strict: false });
const validateLegacySubset = ajv.compile(legacyConsumerSubsetSchema);

test('a v2 response still satisfies the v1 consumer subset', () => {
  const response = buildOrderResponse({ schemaVersion: 2 });
  const { schemaVersion, ...legacyFields } = response;
  const isValid = validateLegacySubset(legacyFields);

  assert.equal(isValid, true, JSON.stringify(validateLegacySubset.errors));
});

Das Sub-Schema deklariert nur die Felder und Typen, die der alte Konsument liest, und lässt jedes neue Feld durch. Das fängt entfernte Felder und Typänderungen ab, ohne Fehlalarme bei Ergänzungen. Halten Sie Fall-Daten in einem Fixtures-Verzeichnis, geordnet und diffbar; zu Merge-Konflikten in diesen Dateien siehe Git-Konflikte in JSON-Dateien: union schreibt kaputtes JSON, nach Schlüssel mergen.

8. Canary-Rollout: nach Dimension skalieren, bei Metriken stoppen

Kompatibilitätsprobleme zeigen sich selten in Unit-Tests; sie zeigen sich in einem Canary. Erweitern Sie die Exposition von eng zu breit:

Dimension Praxis Was sie validiert
Tenant-Allowlist Den neuen Lesepfad zuerst für interne oder befreundete Kunden aktivieren Unterschiede in der Form echter Daten
Client-Version Nur für Clients aktivieren, die bereits auf die neue Form upgegradet sind Alte und neue Clients laufen Seite an Seite
Traffic-Prozentsatz 5 % → 25 % → 100 %, mit Beobachtung eines vollen Geschäftszyklus pro Schritt Long-Tail-Aufrufe und Cache-Hit-Verhalten

Beobachten Sie mindestens drei Metriken: API-Fehlerrate, Rate der Vertragsvalidierungsfehler und das Lese-Verhältnis zwischen alten und neuen Feldern. Definieren Sie automatische Stoppbedingungen, etwa 5xx über 1 % oder ein plötzliches Wiederansteigen von Lesezugriffen auf alte Felder, und rollen Sie das Flag zurück. Halten Sie Doppelschreibvorgänge während des Canarys aktiv, damit ein Rollback nur das Umschalten des Lesepfads erfordert, nicht die Reparatur von Daten.

9. Eine Kompatibilitätscheckliste vor der Auslieferung

  • [ ] Neue Felder sind optional, semantisch unabhängig und verwenden keine alten Namen wieder.
  • [ ] Konsumenten behandeln unbekannte Felder und unbekannte Enum-Werte mit einem Fallback.
  • [ ] Das Versionsfeld wird mit dem Dokument persistiert und ist auf der Leitung verhandelbar.
  • [ ] Das Veraltungsfenster hat eine Ankündigung, einen Sunset-Header und Beobachtung.
  • [ ] Lesezugriffe auf alte Felder lassen sich über Logs oder Traces auf konkrete Aufrufer zurückführen.
  • [ ] Das Migrationsskript ist idempotent, gebatcht, wiederaufnehmbar und hat einen Rollback-Pfad.
  • [ ] Vertragstests decken das Legacy-Consumer-Subset ab und laufen in CI.
  • [ ] Der Canary hat ausdrückliche Expositionsdimensionen und Stoppmetriken.

FAQ

Ist das Hinzufügen eines optionalen Feldes immer rückwärtskompatibel?

Nein. Es ist nur kompatibel, wenn Konsumenten unbekannte Felder ignorieren und strenge Schema-Validierung vermeiden. Ein Konsument mit additionalProperties: false oder positionsbasiertem Lesen scheitert weiterhin. Prüfen Sie seine Toleranz mit einem Vertragstest, bevor Sie ausliefern.

Sollte schemaVersion in der URL oder im Response-Body leben?

Die Leitung kann Versionen über Medientypen oder URL-Pfade verhandeln, aber gespeicherte Datensätze müssen schemaVersion im Dokument tragen. Queues, Offline-Backups und Audits passieren nie eine URL, nur eine Version im Dokument kann Replay und Backfill unterstützen.

Kann ein veraltetes Feld sofort gelöscht werden?

Nein. Hören Sie auf, es zu schreiben, kündigen Sie das Entfernungsdatum an und zählen Sie Lesezugriffe pro Aufrufer. Löschen Sie erst, nachdem das Fenster geschlossen ist und Lesezugriffe bei null bleiben. Führen Sie vor dem Löschen eine vollständige Regression aus, um zu bestätigen, dass kein versteckter Cron-Job oder Bericht es noch liest.

Was, wenn ein Migrationsskript zur Hälfte scheitert?

Entwerfen Sie es als idempotenten Job: bedingte Updates, ein Primärschlüssel-Cursor und Checkpoints. Setzen Sie nach einem Fehler vom Checkpoint fort, und wiederholte Läufe dürfen nicht doppelt schreiben. Für den Rollback behalten Sie das alte Feld und eine Momentaufnahme; setzen Sie nicht einfach schemaVersion zurück.

Wie sollte ich eine Datenmigration sequenzieren?

Folgen Sie Parallel Change: zuerst expand, damit alte und neue Felder koexistieren und beide Versionen gelesen werden können, historische Datensätze in die neue Form backfillen, dann durch Entfernen des alten Felds und der Kompatibilitätszweige contracten. Kehren Sie die Reihenfolge nicht um, sonst verlieren Sie den Rollback-Pfad, und machen Sie das Migrationsskript idempotent, gebatcht und wiederaufnehmbar.