JSON lesen

Die sechs JSON-Typen und die Werte, an denen Tests scheitern.

Die meisten APIs, die du testen wirst, senden und empfangen JSON. Es ist ein Textformat für strukturierte Daten und klein genug, um es an einem Nachmittag zu lernen. Die Syntax ist selten das Problem. Das Problem ist, dass JSON ein paar Dinge offenlässt und verschiedene Programme diese Lücken unterschiedlich füllen. In diesen Lücken stecken viele Bugs, deshalb verbringt diese Lektion mehr Zeit mit ihnen als mit der Syntax.

Sechs Arten von Werten

Ein JSON-Wert ist immer einer von diesen:

TypBeispielHinweise
Objekt{ "key": "value" }Benannte Felder in geschweiften Klammern. Schlüssel sind immer Strings in doppelten Anführungszeichen.
Array[1, 2, 3]Eine geordnete Liste in eckigen Klammern. Die Elemente können jeden Typ haben, gemischt.
String"hello"Text in doppelten Anführungszeichen. Einfache Anführungszeichen sind nicht erlaubt.
Zahl42, -7, 3.14159, 6.022e23Ein Typ für Ganz- und Dezimalzahlen. Keine führenden Nullen, kein NaN, kein Infinity.
Booleantrue, falseKleingeschrieben, ohne Anführungszeichen.
NullnullKleingeschrieben, ohne Anführungszeichen. Bedeutet „kein Wert“.

Das ist das ganze Typsystem. Es gibt keinen Datumstyp, keinen von Dezimalzahlen getrennten Ganzzahltyp und keine Möglichkeit, Kommentare einzufügen. Diese API hat einen Endpunkt, der von allem eins liefert:

GET/api/edge-cases/types
Dieser Request nutzt einen Endpunkt für angemeldete Mitglieder.AnmeldenKostenloses Konto erstellen
JSON
{
  "string": "hello",
  "emptyString": "",
  "integer": 42,
  "negativeInteger": -7,
  "float": 3.14159,
  "exponent": 6.022e23,
  "booleanTrue": true,
  "booleanFalse": false,
  "nullValue": null,
  "array": [1, 2, 3],
  "emptyArray": [],
  "object": { "key": "value" },
  "emptyObject": {},
  "arrayOfObjects": [ { "id": 1, "name": "Alice" }, { "id": 2, "name": "Bob" } ],
  "nestedArrays": [[1, 2], [3, [4, [5]]]],
  "stringThatLooksLikeNumber": "123",
  "stringThatLooksLikeBoolean": "true",
  "stringThatLooksLikeNull": "null"
}

Sieh dir die letzten drei Felder an. "123", "true" und "null" sind alles Strings, weil sie in Anführungszeichen stehen. Ein Mensch, der die Antwort liest, sieht eine Zahl, einen Boolean und ein null. Ein Programm sieht drei Strings. Wenn du eine Antwort prüfst, prüf den Typ genauso wie den Wert. Eine API, die heute "price": "249.99" liefert und morgen "price": 249.99, hat ihren Vertrag geändert, auch wenn die Zahl gleich aussieht.

Auf einen Wert zeigen

Wenn du einen Test oder einen Bug-Report schreibst, brauchst du eine Möglichkeit zu sagen, welchen Wert du meinst. Hier ist ein Teil des Produkts aus der ersten Lektion:

JSON
{
  "id": 1,
  "title": "Wireless Noise-Cancelling Headphones",
  "price": 249.99,
  "category": "electronics",
  "rating": { "rate": 4.3, "count": 127 },
  "inStock": true,
  "createdAt": "2025-06-12T10:30:00Z"
}

Die übliche informelle Schreibweise nutzt einen Punkt, um in ein Objekt hineinzugehen, und eckige Klammern mit einer Zahl, um ein Element aus einem Array zu wählen. Gezählt wird ab null.

PfadWert
title"Wireless Noise-Cancelling Headphones"
rating.rate4.3
rating.count127

Wenn die ganze Antwort ein Array ist, wie bei /api/Products?limit=2, ist die ID des ersten Produkts [0].id und die Bewertung des zweiten [1].rating.rate. Werkzeuge schreiben das leicht unterschiedlich. JSONPath, das dir in Postman, REST Assured und vielen Testbibliotheken begegnet, beginnt mit einem Dollarzeichen für die Wurzel: $.rating.rate, $[0].id. Es hat auch Platzhalter, $[*].pricebedeutet also "der Preis jedes Elements". JMESPath, das die AWS-Kommandozeilenwerkzeuge nutzen, schreibt dasselbe als [*].price. Du musst noch keins davon im Detail lernen. Zu wissen, dass $[0].id"die id des ersten Elements" bedeutet, reicht, um den meisten Testcode zu lesen.

Null, fehlend und leer

Das sieht ähnlich aus und bedeutet Verschiedenes. Angenommen, ein Nutzer hat keinen zweiten Vornamen. Eine API könnte das auf mindestens fünf Arten sagen:

  • "middleName": null, das Feld existiert und hat keinen Wert
  • gar kein Schlüssel middleName
  • "middleName": "", ein leerer String
  • "middleName": "null", das vierbuchstabige Wort null, meist ein Bug, bei dem etwas toString auf einem null aufgerufen hat
  • "middleName": "None" oder "undefined", derselbe Bug aus Python oder JavaScript

Dieser Endpunkt liefert alle nebeneinander:

GET/api/edge-cases/nulls
Dieser Request nutzt einen Endpunkt für angemeldete Mitglieder.AnmeldenKostenloses Konto erstellen
JSON
{
  "nullValue": null,
  "nestedNull": { "a": null, "b": { "c": null } },
  "arrayOfNulls": [null, null, null],
  "arrayWithNull": [1, null, 3],
  "nullAsString": "null",
  "undefinedAsString": "undefined",
  "noneAsString": "None",
  "emptyString": "",
  "emptyArray": [],
  "emptyObject": {},
  "zero": 0,
  "false": false
}

Welche davon richtig ist, hängt von der Dokumentation der API ab, und die sagt es oft nicht. Was du testen kannst, ist Konsistenz. Hat ein Produkt "discount": null und ein anderes gar keinen Schlüssel discount, stürzt ein Client, der product.discount.amount liest, beim einen ab und beim anderen nicht. Achte auch darauf, ob 0 und falsefür "unbekannt" verwendet werden. Ein Bestand von 0 und ein Bestand, den niemand kennt, sind verschiedene Tatsachen.

Zahlen

JSON begrenzt weder Größe noch Genauigkeit einer Zahl. Die Programme, die JSON lesen, schon. JavaScript speichert jede Zahl als 64-Bit-Float, der Ganzzahlen nur bis 9.007.199.254.740.991 exakt halten kann. Darüber rundet er stillschweigend.

GET/api/edge-cases/numbers
Dieser Request nutzt einen Endpunkt für angemeldete Mitglieder.AnmeldenKostenloses Konto erstellen
JSON (gekürzt)
{
  "int64Max": 9223372036854775807,
  "int64MaxAsString": "9223372036854775807",
  "maxSafeInteger": 9007199254740991,
  "beyondSafeInteger": 9007199254740993,
  "beyondSafeIntegerAsString": "9007199254740993",
  "pointOnePlusPointTwo": 0.30000000000000004,
  "money": 19.99,
  "moneyAsString": "19.99",
  "trailingZeros": 1.50,
  "negativeZero": -0,
  "numberAsString": "42",
  "nanAsString": "NaN"
}

Parse diese Antwort in JavaScript und in Python und vergleiche:

JavaScript
JSON.parse(body).beyondSafeInteger   // 9007199254740992
JSON.parse(body).int64Max            // 9223372036854776000
Python
json.loads(body)["beyondSafeInteger"]   # 9007199254740993
json.loads(body)["int64Max"]            # 9223372036854775807

JavaScript hat 9007199254740993 ohne jeden Fehler in 9007199254740992 verwandelt. Python hat den Wert exakt behalten. Nutzt eine API 64-Bit-Ganzzahlen als IDs (die von Twitter taten das, viele Datenbanken auch), kann ein JavaScript-Client am Ende den falschen Datensatz anfordern. Deshalb schicken sorgfältige APIs große IDs als Strings, wie int64MaxAsString oben. Wenn du eine API mit großen numerischen IDs testest, prüf, was ein Browser-Client wirklich bekommt.

Dezimalzahlen haben ein verwandtes Problem. 0.1 + 0.2 ergibt in fast jeder Sprache 0.30000000000000004, weil binäre Floats 0.1 nicht exakt darstellen können. Bei Geld ist das wichtig. Manche APIs schicken Preise als Zahlen (19.99), manche als Strings ("19.99"), manche als ganze Cent (1999). Jede Variante kann funktionieren. Ein Test sollte prüfen, dass Summen auf den Cent aufgehen, nicht, dass sie "nah genug" sind.

Zwei kleinere Dinge aus derselben Antwort. 1.50 und 1.5 sind nach dem Parsen dieselbe Zahl, ein Test, der rohen Text vergleicht, scheitert also, wo ein Test, der Werte vergleicht, besteht. Und -0 ist gültiges JSON, aber JavaScripts JSON.stringify(-0) schreibt 0, es übersteht also keine Hin- und Rückumwandlung.

Daten sind Strings

JSON hat keinen Datumstyp, also reisen Daten als Strings oder Zahlen, und jede API wählt ihr eigenes Format. Hier ist ein Zeitpunkt, auf mehrere Arten geschrieben:

GET/api/edge-cases/dates
Dieser Request nutzt einen Endpunkt für angemeldete Mitglieder.AnmeldenKostenloses Konto erstellen
JSON (gekürzt)
{
  "iso8601Utc": "2025-07-15T09:30:00Z",
  "iso8601PositiveOffset": "2025-07-15T15:00:00+05:30",
  "iso8601NoZone": "2025-07-15T09:30:00",
  "dateOnly": "2025-07-15",
  "unixSeconds": 1752571800,
  "unixMilliseconds": 1752571800000,
  "usFormat": "07/15/2025",
  "euFormat": "15/07/2025",
  "invalidLeapDay": "2025-02-29T00:00:00Z",
  "nullDate": null,
  "emptyDate": ""
}

Das Format, das du am häufigsten siehst, ist ISO 8601 mit Zeitzone, etwa 2025-07-15T09:30:00Z. Das Z bedeutet UTC. Was du prüfst, wenn eine API Daten liefert:

  • Die Zeitzone ist angegeben. 2025-07-15T09:30:00 ohne Zone bedeutet 9:30 Uhr irgendwo, und jeder Client rät ein anderes Irgendwo.
  • Unix-Timestamps sagen, ob sie Sekunden oder Millisekunden sind. 1752571800 und 1752571800000 sind derselbe Zeitpunkt, und wer sie verwechselt, landet im Jahr 1970 oder im Jahr 57500.
  • 07/15/2025 und 15/07/2025 sind nur eindeutig, weil 15 kein Monat sein kann. 03/04/2025 ist März oder April, je nachdem, wer es liest.
  • Unmögliche Daten wie 2025-02-29 werden von manchen Parsern angenommen und von anderen abgelehnt. Schick sie an eine API, die Daten als Eingabe nimmt.

Reihenfolge und doppelte Schlüssel

Die Schlüssel eines Objekts haben keine Reihenfolge. {"a": 1, "b": 2} und {"b": 2, "a": 1} sind dasselbe Objekt. Die meisten Server schicken die Schlüssel zufällig immer in derselben Reihenfolge, deshalb ist es verlockend, die ganze Antwort als String zu vergleichen. Dieser Test bricht an dem Tag, an dem ein Bibliotheks-Update die Schlüssel umsortiert, obwohl eigentlich nichts falsch ist. Vergleiche stattdessen geparste Werte. Die Reihenfolge in Arrays dagegen zählt: [1, 2] und [2, 1] sind verschiedene Arrays.

Doppelte Schlüssel sind ein merkwürdigerer Fall. Der JSON-Standard sagt, Schlüssel "sollten" eindeutig sein, nicht, dass sie es müssen, also ist das technisch erlaubt:

GET/api/edge-cases/duplicate-keys
Dieser Request nutzt einen Endpunkt für angemeldete Mitglieder.AnmeldenKostenloses Konto erstellen
JSON
{
  "id": 1,
  "name": "first value",
  "name": "second value",
  "nested": { "flag": true, "flag": false },
  "id": 2
}

JavaScripts JSON.parse und Pythons json.loads behalten beide den letzten Wert, was {"id": 2, "name": "second value", "nested": {"flag": false}} ergibt. Andere Parser behalten den ersten Wert oder lehnen das Dokument ab. Wenn ein Gateway den ersten Schlüssel role prüft und das Backend den zweiten liest, wird dieser Unterschied zur Sicherheitslücke. In Antworten begegnen dir doppelte Schlüssel selten, aber es lohnt sich, sie in Requests zu schicken, wenn du die Verarbeitung von Eingaben testest.

Wenn das JSON kaputt ist

Manchmal schickt ein Server etwas, das gar kein gültiges JSON ist: einen abgeschnittenen Body, eine HTML-Fehlerseite, einen Stacktrace. Hier ist eine Antwort, die behauptet, JSON zu sein, und es nicht ist:

Antwort
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id": 1, "name": "Alice", "tags": ["a", "b",], "active": tru

Nach "b" steht ein überzähliges Komma, und der Body endet mitten in true. Der Status ist 200, und der Header Content-Type sagt application/json, also versucht ein Client, ihn zu parsen, und scheitert. Node.js meldet Unexpected token ']'. Python meldet Illegal trailing comma before end of array. Bei einer echten API bedeutet das meist, dass der Server mitten beim Schreiben der Antwort abgestürzt ist oder ein Proxy sie abgeschnitten hat. Die Lehre für Tests: Parse den Body bei jeder Prüfung. Ein 200 mit einem nicht parsbaren Body ist ein Fehlschlag, und ein Test, der nur den Statuscode ansieht, übersieht ihn.

Eine große Antwort lesen

Echte Antworten haben oft Hunderte Zeilen. Bevor du Details prüfst, verschaff dir die Form: Ist die oberste Ebene ein Objekt oder ein Array, wie viele Elemente gibt es, welche Felder hat ein Element. Dann lies ein Element vollständig. Die meisten JSON-Bugs sieht man schon an der Form, etwa eine Liste, die bei einem einzigen Ergebnis als einzelnes Objekt zurückkommt, oder ein Objekt, das an einem Endpunkt in einen zusätzlichen Schlüssel data gepackt ist und an einem anderen nicht.

Übung

Schick GET /api/Countries/code/INanmelden zum Anzeigen und beantworte diese Fragen aus der Antwort. Schreib jeden Pfad in der Punktschreibweise aus dieser Lektion:

  • Welchen Typ hat population, und welchen callingCode? Warum könnten sie sich unterscheiden?
  • Wie lautet der Pfad zum Währungssymbol?
  • Wie lautet der Pfad zur zweiten Sprache?
  • Das Feld flag ist ein Emoji. Sieh dir die rohe Antwort mit curl an und vergleich sie mit dem, was dein Browser zeigt. Was ist anders, und spielt es für einen Client eine Rolle?

Öffne dann /api/edge-cases/stringsanmelden zum Anzeigen und finde mindestens zwei Werte, die im Browser gut aussehen würden, aber ein Programm, das sie speichert oder anzeigt, zum Absturz bringen könnten.

Üben und weitergehen