Startseite / Artikel

Gültiges JSON, fehlerhafter Vertrag: Schichtweise Überprüfungen für Payload-Regressionen

Erlernen Sie, wie man Regressionen im JSON-Payload aufspürt, der sauber parset wird: semantische Unterschiede, ein fokussiertes JSON Schema, Assertionen für Geschäftsregeln in Node sowie die Grenzen der generierten Typen.

Verlag: REACTAPP.TOP7 Min. Lesezeit1572 Wörter

Quellen: Catch JSON Contract Regressions Before They Reach Your App

Eine Antwort kann mit HTTP 200 eintreffen, fehlerfrei interpretiert werden und dennoch Ihre Anwendung zum Ausfall bringen. Die Syntaxvalidierung beweist lediglich, dass es sich um JSON handelt; sie sagt nichts darüber aus, ob der Payload weiterhin mit dem übereinstimmt, was Ihr Code erwartet. In dieser Anleitung wird eine kleine, mehrschichtige Regressionstestmethode erstellt (semantische Vergleichung, fokussiertes Schema sowie explizite Geschäftsregeln), sodass ein Vertragsbruch als präzise Meldung sichtbar wird anstelle eines seltsamen Fehlers weiter im System.

Ein Payload, der interpretiert werden kann, aber dennoch fehlschlägt

Betrachten Sie einen Bestelldienst. Der Endpunkt antwortet erfolgreich, das JSON wird parsen, und anschließend erzeugt der Code, der die Menge der Artikel zusammenzählt, "021" anstelle von 3. Dieses Szenario ist erfunden, doch der Fehlermodus ist häufig: In einem überfüllten Payload sind die Zeichenkette "2" und die Zahl 2 leicht übersehbar, beide sind gültiges JSON, und nur eine von ihnen entspricht den Vertragsbedingungen.

Die untenstehende Konfiguration stellt die eingehende Bestellung dar. Schauen Sie sich den ersten Artikel genau an: Seine quantity ist eine eingeklammerte Zeichenkette, während der zweite Artikel eine einfache Zahl verwendet. Alles Weitere sind plausible Bestelldaten, einschließlich einer null-Versandanweisung und eines im Voraus berechneten totalCents.

{
  "orderId": "ORD-1042",
  "createdAt": "2026-09-21T09:30:00Z",
  "customer": {"id": "C-017", "name": "Avery Example", "vip": false},
  "items": [
    {"sku": "MUG-BLUE", "quantity": "2", "unitPriceCents": 1250},
    {"sku": "NOTE-A5", "quantity": 1, "unitPriceCents": 650}
  ],
  "shipping": {"city": "Exampleton", "instructions": null},
  "paid": true,
  "totalCents": 3150
}

Eine strenge Syntaxprüfung akzeptiert dieses Dokument ohne Einwände, und das ist ein legitimes Ergebnis: Der Text entspricht der JSON-Grammatik in RFC 8259, wobei Strings, Zahlen, Boolesche Werte sowie null alle gültige Werte sind. Was die Grammatik jedoch nicht erkennen kann, sind die Regeln, die Ihre Anwendung hinzufügt:

  • quantity muss eine positive Ganzzahl sein
  • paid muss ein Boolescher Wert sein
  • Die Summe aus jeder Menge multipliziert mit ihrem Einzelpreis muss totalCents entsprechen

Nehmen wir an, die vorherige Version dieser Bestellung unterschied sich in genau zwei Punkten: quantity hatte den Wert 2 und paid war false. Die Änderung des Zahlungsflags auf true ist eine erwartete Zustandsänderung. Dass die Menge von einer Zahl zu einem String wird, stellt hingegen eine Regression dar.

Differenzwerte nach Pfad, nicht nach Textzeilen

Es gibt zwei sehr unterschiedliche Fragen, die man stellen kann, wenn man Payloads vergleicht. Ein semantischer Vergleich analysiert beide Dokumente und vergleicht die Werte an jedem Pfad. Dabei wird die Reihenfolge der Objektschlüssel als unerheblich angesehen, sodass das Vertauschen der Positionen von customer.id und customer.name keine Änderung darstellt, während die Reihenfolge in Arrays als bedeutend gilt, wodurch das Verschieben eines Elements an einen anderen Index eine Änderung sein kann. Für dieses Testbeispiel gibt ein semantischer Diff genau zwei Pfade an: $.items[0].quantity und $.paid. Damit wird die Frage „Welche Daten haben sich geändert?“ beantwortet.

Ein Textdiff beantwortet eine andere Frage: „Welche Zeichen oder Zeilen haben sich geändert?“ Er bleibt dennoch ein nützliches Beweismittel, und die Normalisierung der Einrückung sowie des Formats auf beiden Seiten beseitigt einen Großteil des „Rauschens“. Allerdings ist übereinstimmender oder unterschiedlicher Text kein Beweis dafür, dass zwei Dokumente denselben oder einen anderen JSON-Wert darstellen, denn Leerzeichen, Schlüsselreihenfolge und Zahlendarstellung können sich ändern, ohne die Bedeutung zu verändern.

Die Überprüfungen in reinem Node nachvollziehen

Man benötigt keinen externen Dienst, um diese Fakten zu überprüfen. Der unten stehende Skript liest order.json aus seinem eigenen Verzeichnis (über import.meta.url) ein, rekonstruiert die vorherige Version mit structuredClone und prüft die Unterschiede mithilfe von node:assert/strict. Anschließend wird der Fehler demonstriert, die Testdatei auf einer Kopie korrigiert und geprüft, ob die berechnete Gesamtsumme mit totalCents übereinstimmt. Speichern Sie es als check-order.mjs neben der Testdatei und führen Sie node check-order.mjs aus.

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const incoming = JSON.parse(
  readFileSync(new URL('./order.json', import.meta.url), 'utf8')
);
const before = structuredClone(incoming);
before.items[0].quantity = 2;
before.paid = false;
assert.equal(before.items[0].quantity, 2);
assert.equal(incoming.items[0].quantity, '2');
assert.equal(before.paid, false);
assert.equal(incoming.paid, true);
const naiveCount = incoming.items.reduce(
  (sum, item) => sum + item.quantity,
  0
);
assert.equal(naiveCount, '021');
const corrected = structuredClone(incoming);
corrected.items[0].quantity = 2;
const count = corrected.items.reduce((sum, item) => sum + item.quantity, 0);
const computedTotal = corrected.items.reduce(
  (sum, item) => sum + item.quantity * item.unitPriceCents,
  0
);
assert.equal(count, 3);
assert.equal(computedTotal, corrected.totalCents);
console.log('Contract checks passed');

Die Assertion naiveCount ist die interessante. Die Reduktion beginnt mit der Zahl 0, fügt den String "2" hinzu und erhält so den String "02", da + bei einem der Operanden ein String auf Concatenation umschaltet. Im nächsten Schritt wird 1 angehängt, wodurch "021" entsteht. Ein numerischer Anfangswert zwingt spätere Operanden nicht zur Umwandlung; sobald ein String in den Akkumulator gelangt, werden alle nachfolgenden Werte zusammengefügt.

Achten Sie darauf, was das Skript nicht empfiehlt. Die Anpassung des Fixtures auf eine Zahl ist eine Möglichkeit, den korrekten Vertrag darzustellen – nicht ein Hinweis darauf, jedes eintreffende Feld in Number() zu packen. Stille Zwangsumwandlungen verbergen Fehler bei den Erzeugern. Entweder beheben Sie den Dienst, der den String ausgibt, oder definieren Sie eine explizite, dokumentierte Normalisierungsregel an der Stelle, an der die Daten in Ihr System gelangen.

Umwandlung der Typregel in ein Schema

Prosaartige Regeln sind leicht zu vergessen; ausführbare hingegen nicht. Das untenstehende Schema aus Draft 2020-12 kodiert die Anforderungen, die für diese Regression relevant sind. Es erfordert orderId, items, paid und totalCents, verlangt mindestens ein Element und beschränkt quantity auf eine ganze Zahl mit einem Minimum von 1 sowie unitPriceCents auf eine nicht-negative ganze Zahl.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["orderId", "items", "paid", "totalCents"],
  "properties": {
    "orderId": {"type": "string"},
    "items": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": ["sku", "quantity", "unitPriceCents"],
        "properties": {
          "sku": {"type": "string"},
          "quantity": {"type": "integer", "minimum": 1},
          "unitPriceCents": {"type": "integer", "minimum": 0}
        }
      }
    },
    "paid": {"type": "boolean"},
    "totalCents": {"type": "integer", "minimum": 0}
  }
}

Die Überprüfung des eintreffenden Datensatzes gegen dieses Schema scheitert mit einer Meldung wie $.items[0].quantity: erwarteter Typ ist Ganzzahl. Wenn man diesen einzigen Wert in die Zahl 2 ändert, gelingt die Überprüfung. Genau diesen spezifischen Pfad möchte man bei einer Regressionstestung haben.

Wissen, was Ihr Validator tatsächlich abdeckt

Validator unterscheiden sich darin, wie viel der Spezifikation sie umsetzen. Einige überspringen absichtlich die Verarbeitung von entfernten $ref-Zielen, die Auswertung von pattern-Regulärausdrücken oder die Überprüfung von format-Schlüsselwörtern, und standardmäßig betrachtet die Spezifikation format als Annotation statt als Aussage. Stellen Sie sicher, dass der Validator, den Sie in Tests verwenden, dieselben Schlüsselwörter abdeckt, auf denen Ihr Produktionskontrakt beruht – andernfalls kann ein Schema strenger erscheinen als die Kontrollen, die es durchsetzen.

Grenzfälle, für die jeweils ein Test sinnvoll ist

Ein guter Kontrakt lehnt fehlerhafte Eingaben aus unterschiedlichen Gründen ab. Jeder dieser Fälle sollte für sich allein fehlschlagen:

  • items ist gänzlich fehlend (Verstoß gegen required)
  • items ist ein leeres Array (minItems)
  • quantity beträgt null (minimum)
  • paid wird als Zeichenkette "true" (type) übermittelt
  • Ebenso wichtig ist, was übermittelt werden sollte: eine unbezahlte Bestellung mit paid: false. Das Schlüsselwort required prüft lediglich, ob die Eigenschaft vorhanden ist, nicht jedoch, ob ihr Wert wahr ist. Die offiziellen JSON Schema-Anleitungen erklären, wie required mit Objekt-eigenschaften zusammenhängt und wie jedes Array-Element gegen das items-Subschema überprüft wird.

    Was das Schema absichtlich weglässt

    Dies ist ein zielgerichtetes Schema, keine vollständige Bestellspezifikation. Kundendaten, Versand, Zeitstempelsemantik sowie zusätzliche Felder sind nicht eingeschränkt. Auch die arithmetische Regel fällt absichtlich außerhalb des Anwendungsbereichs: Ob die Posten auf totalCents addieren, ist eine Anwendungslogik, die ein standardmäßiges JSON Schema nicht ausdrücken kann; deshalb prüft das Node-Skript dies separat. Durch die Trennung beider Komponenten erledigt jedes Werkzeug das, wofür es am besten geeignet ist.

    Die generierten Typen beschreiben ein Beispiel

    Es ist verlockend, eine Payload in einen JSON-zu-TypeScript-Generator einzufügen und das Ergebnis als Vertragsdokument zu betrachten. Bei Ausführung am korrigierten Testfall liefert ein solcher Generator innerhalb der verschachtelten Typen Werte wie quantity: number, createdAt: string und instructions: null. Das sind genaue Beschreibungen dieses einen Beispiels und nichts weiter:

    • JSON verfügt über nur einen Zahlentyp, daher kann number keinen „positiven ganzen Zahlen“-Begriff darstellen.
    • Ein einzelnes Beispiel kann nicht zeigen, welche Felder optional sind.
    • Ein Feld, das heute zufällig den Wert null hat, kann morgen legitimerweise einen Zeichenkettenwert enthalten.
    • Die Typen von TypeScript verschwinden zur Laufzeit und überprüfen niemals die eingehenden Bytes; das Handbuch zu Type Assertions in TypeScript besagt ausdrücklich, dass Assertionen keine Laufzeitprüfungen durchführen.

    Falls Sie eine einzige Definition benötigen, die sowohl die Laufzeitvalidierung als auch die statischen Typen steuert, lesen Sie unseren Artikel über das Teilen eines Zod-Schemas zwischen Frontend und Backend.

    Auch an den Grenzen muss Präzision herrschen

    Zahlen stellen eine weitere Falle dar. JavaScript interpretiert {"id":9007199254740993} als 9007199254740992, weil der Wert den Wert von Number.MAX_SAFE_INTEGER überschreitet und auf das nächstgelegene darstellbare Double gerundet wird. Wenn ein Identifikator alle Ziffern beibehalten muss, sollte der Vertragsentwurf ihn als String definieren. Es ist zu spät, dies nach JSON.parse noch zu korrigieren – die Ziffern sind bereits verschwunden.

    Wichtige Erkenntnisse

    • Bewahren Sie eine saubere Version eines bekannten, einwandfreien Payloads auf und verwenden Sie niemals echte Anmeldeinformationen oder persönliche Daten in Testdaten.
    • Betrachten Sie Überprüfungen als Schichten: strenge Syntax-Prüfung, semantische Wertvergleiche, Schema-Validierung sowie anschließend Geschäftsregeln, die das Schema nicht ausdrücken kann.
    • Erstellen Sie TypeScript-Typen erst nach Abschluss des Vertragsentwurfs und betrachten Sie sie niemals als Ersatz für eine Laufzeitvalidierung.
  • Zielen Sie auf Fehler ab, die einen Pfad und eine Ursache angeben, wie zum Beispiel „$.items[0].quantity wurde von einer Ganzzahl in einen String geändert, wodurch die Validierung fehlschlägt und die Addition zusammengefügt wird“, anstatt „die Reihenfolge sieht falsch aus“.