JSON Toolkit

Sechs Typen, Zahlenpräzision und Kodierungsfallen

Fazit: JSON kennt nur sechs Typen, aber jeder davon kann Ihre Daten still umformen. Dieser Artikel behandelt die Fallen von Zahlen, Strings und null in einem Durchgang.

Einsteiger~12 Min. LesezeitVeröffentlicht 2026-10-30Aktualisiert 2026-10-30
Titelbild für Sechs Typen, Zahlenpräzision und Kodierungsfallen

Kurz gesagt: Prüfen Sie zuerst, ob ID- oder Geldbetragsfelder 2^53 - 1 überschreiten oder viele Nachkommastellen Präzision benötigen, und dann, ob String-Limits Codepoints oder UTF-16-Einheiten zählen; führen Sie jeden Wert jenseits des sicheren Ganzzahlbereichs als String, statt dem Parser zu vertrauen, dass er die Präzision bewahrt.

JSON hat weniger Typen, als die meisten Menschen denken, und mehr Fallen: Zahlen verschieben sich, Strings brechen, und null ist etwas anderes als ein fehlender Schlüssel.

1. Die Typenlandschaft

Typ Literalbeispiel Einschränkung in JSON Übliche Abbildung
Objekt {"a": 1} Schlüssel müssen Strings sein, Werte beliebig Object / Map / struct
Array [1, "a", null] Elemente dürfen heterogen sein Array / List / slice
String "text" UTF-8, doppelte Anführungszeichen String
Zahl 1, -1.5, 1e9 Keine führenden Nullen, kein +, kein NaN double / Decimal
Boolean true / false Kleinschreibung bool
null null Der einzige Nullwert null / Option::None

Beachten Sie die beiden Dinge, die es nicht gibt: Es gibt keinen Datumstyp und keinen Ganzzahltyp. Alle Daten sind Strings, und alle Ganzzahlen und Gleitkommawerte teilen sich den einzigen Typ „Zahl“.

2. Zahlen: Doppelte Präzision ist die Standardantwort

2.1 Die Grenze der sicheren Ganzzahl

Die größte Ganzzahl, die IEEE 754 doppelte Präzision exakt darstellen kann, ist 2^53 - 1 = 9007199254740991. Darüber hinaus entstehen Lücken zwischen benachbarten Ganzzahlen:

JSON.parse('{"id": 9007199254740993}').id
// 9007199254740992

JSON.stringify({ id: 9007199254740993 })
// '{"id":9007199254740992}'

Das ist kein JSON-Problem, sondern eines des Schritts „Text → doppelte Präzision“. Jede Pipeline mit JavaScript in der Mitte (Gateway, BFF, Frontend, Node-Dienst) stößt darauf.

Abhilfen (nach Empfehlung sortiert):

  1. IDs über die gesamte Pipeline als Strings verwenden: {"id": "9007199254740993"}, der geringste Aufwand von allen;
  2. Auf dem Server mit Typen hoher Präzision parsen: Javas BigInteger / BigDecimal, Gos json.Number, Pythons decimal.Decimal;
  3. Im Frontend den Reviver von JSON.parse oder einen Parser mit Unterstützung großer Zahlen (etwa json-bigint) nutzen, um das Feld in BigInt zu verwandeln – beachten Sie aber, dass BigInt nicht direkt an JSON.stringify übergeben werden kann.

2.2 Dezimalzahlen sind keine exakten Werte

0.1 + 0.2            // 0.30000000000000004
JSON.stringify(0.1 + 0.2)
// "0.30000000000000004"

Stellen Sie Geldbeträge stets als Ganzzahlen in der kleinsten Währungseinheit (Cent) oder als Strings dar; akkumulieren Sie sie niemals mit Gleitkommazahlen.

2.3 Fallen bei der Serialisierung von Zahlen

JSON.stringify({ a: 1e21 })        // {"a":1e+21}
JSON.stringify({ a: -0 })          // {"a":0}
JSON.stringify({ a: Infinity })    // {"a":null}
JSON.stringify({ a: NaN })         // {"a":null}
JSON.stringify({ a: 10n })         // TypeError: Do not know how to serialize a BigInt

Infinity und NaN werden stillschweigend zu null – die am schwersten aufzuspürende Klasse von „die Daten sind einfach verschwunden“.

2.4 Standardverhalten nach Sprache

Sprache Standard-Zahlentyp Fälle mit expliziter Behandlung
JavaScript double Große Ganzzahlen, BigInt-Serialisierung
Go float64 (encoding/json) json.Number oder Decoder.UseNumber() zur Präzisionserhaltung
Java (Jackson) int/long/double/BigDecimal aus dem Literal abgeleitet DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS erzwingen
Python int (beliebige Präzision) / float parse_float=Decimal zur Erhaltung der Dezimalpräzision
Rust (serde_json) i64/u64/f64 Beliebige Präzision erfordert arbitrary_precision

3. Strings: ein UTF-8-Byte-Stream, betrachtet durch UTF-16

3.1 Länge ist nicht gleich Zeichenzahl

In JavaScript ist "🎉".length gleich 2, weil Emojis in den supplementären Ebenen liegen und durch ein Surrogatpaar dargestellt werden. Wenn der Server die Länge über „Zeichenzahl“ begrenzt, während das Frontend über length validiert, werden sich beide Seiten garantiert uneinig.

'🎉'.length                 // 2
Array.from('🎉').length     // 1
[...'👨‍👩‍👧'].length           // 5 (including zero-width joiners)

Wenn Sie streng zählen müssen, verwenden Sie Intl.Segmenter (nach Graphem-Clustern) oder zählen Sie Unicode-Codepoints im Backend – und legen Sie die Definition klar in der API-Dokumentation fest.

3.2 Einzelne Surrogate

Ein alleinstehendes hohes Surrogat wie "\ud800" ist grammatisch gültiges JSON (\uXXXX akzeptiert beliebige vier Hexziffern), aber kein gültiger Unicode-Text. Seit ES2019 escaped JSON.stringify einzelne Surrogate als \ud800 und garantiert so eine „wohlgeformte“ Ausgabe. Beim sprachübergreifenden Transfer werden solche Werte abgelehnt, wenn die Gegenseite Unicode streng validiert.

3.3 \u2028 und \u2029

Zeilen- und Absatztrenner sind in JSON gültig, aber wenn man sie direkt in JavaScript-Quelltext oder ein <script>-Tag einbettet, entsteht ein Syntaxfehler. Beim Inlining von JSON in HTML stets escapen oder auf das Parsen aus einem String mit JSON.parse umstellen.

3.4 Drei Ursachen für verstümmelten chinesischen Text

  1. Die Datei selbst ist GBK und wird als UTF-8 gelesen;
  2. Der Response-Header deklariert charset=gbk, der Inhalt ist aber tatsächlich UTF-8;
  3. Der Text durchläuft eine Middleware, die „in einen String dekodiert und dann erneut kodiert“, sodass der chinesische Text ein zweites Mal umkodiert wird.

Untersuchungsreihenfolge: zuerst die Bytes ansehen (xxd), dann den Response-Header, zuletzt die Middleware.

4. null versus „dieser Schlüssel existiert nicht“

In JSON sind diese beiden verschieden:

{"nickname": null}
{}
  • Ersteres bedeutet „das Feld existiert, mit einem leeren Wert“;
  • Letzteres bedeutet „das Feld existiert nicht“.

In TypeScript wird daraus der Unterschied zwischen string | null und nickname?: string; in Java der zwischen „explizites null“ und „Standardwert“. Ohne vereinbarte Konvention entstehen diese Abweichungen:

// Frontend A's approach
if (data.nickname === undefined) { /* treat it as having no nickname */ }
// Frontend B's approach
if (!data.nickname) { /* handle null and the empty string together */ }

Empfehlung: Die API-Dokumentation sollte festlegen „bei fehlendem Wert einheitlich null verwenden, niemals den Schlüssel weglassen“ – oder das Gegenteil, aber nur eine Variante wählen; bei der Aktualisierung einer API PATCH-Semantik nutzen, um die Mehrdeutigkeit „nicht gesendet oder als null gesendet“ zu vermeiden.

5. Arrays und Objekte

5.1 Arrays sind nicht „homogen“

[1, "two", {"three": 3}, null]

Grammatisch gültig, aber die Typdefinition wird sehr schwer zu schreiben. In APIs sollten Array-Elemente möglichst homogen bleiben; bei heterogenen Fällen auf ein Array aus Objekten mit einem type-Diskriminantenfeld umstellen.

5.2 Objektschlüssel sind ungeordnet, doch viele verlassen sich auf die Reihenfolge

Die Spezifikation bezeichnet ein Objekt als „eine ungeordnete Sammlung von Name/Wert-Paaren“. In der Praxis bewahrt V8 die Einfügereihenfolge (außer bei Ganzzahlschlüsseln, die sortiert werden). Logik auf die Schlüsselreihenfolge zu stützen (etwa „den ersten Schlüssel nehmen“) heißt, sich auf ein Implementierungsdetail zu verlassen.

5.3 Sparse Arrays und Lücken gibt es nicht

JavaScripts [1, , 3] serialisiert zu [1,null,3] – die Lücke wird zu null. JSON kennt kein Konzept eines „Slots“.

6. Eine Checkliste zum Umsetzen

  • [ ] Alle IDs, Geldbeträge und übergroßen Ganzzahlen sind im API-Vertrag ausdrücklich Strings;
  • [ ] Die API-Dokumentation legt die Semantik von null gegenüber einem weggelassenen Feld fest;
  • [ ] Längenlimits für Strings geben an, ob sie Codepoints oder UTF-16-Einheiten zählen;
  • [ ] Infinity / NaN / undefined werden vor der Serialisierung gefiltert oder ausdrücklich in null umgewandelt;
  • [ ] In HTML eingebettetes JSON wird escaped;
  • [ ] Frontend und Backend haben je einen Vertragstestfall für „große Ganzzahl + Emoji + null“.

FAQ

Wie sollte ich Ganzzahlen oberhalb von 2^53 - 1 behandeln?

Am wenigsten Mühe bereitet es, Felder wie IDs über die gesamte Pipeline als Strings zu senden, zum Beispiel {"id": "9007199254740993"}. Auf dem Server lässt sich die Präzision mit Javas BigInteger / BigDecimal, Gos json.Number oder Pythons decimal.Decimal bewahren. Im Frontend kann ein Parser wie json-bigint das Feld in BigInt verwandeln – denken Sie aber daran, dass BigInt nicht direkt an JSON.stringify übergeben werden kann.

Warum werden Infinity und NaN nach der Serialisierung zu null?

Der JSON-Zahlentyp akzeptiert keine nicht-standardisierten Literale wie NaN und Infinity, deshalb wandelt JSON.stringify sie stillschweigend in null um, ohne Fehler. Das ist die am schwersten aufzuspürende Klasse von „die Daten sind einfach verschwunden“. Filtern Sie solche Werte ausdrücklich vor der Serialisierung oder wandeln Sie sie gemäß Ihrer Geschäftskonvention in null um und protokollieren Sie es.

Soll ich null senden oder das Feld ganz weglassen?

Beides funktioniert, aber die API-Dokumentation muss eine Variante wählen und klar festlegen, sonst nimmt jede Frontend-Instanz etwas anderes an. In den Beispielen entsprechen {"nickname": null} und {} den TypeScript-Typen string | null und nickname?: string. Nutzen Sie beim Aktualisieren einer API PATCH-Semantik, um die Mehrdeutigkeit „nicht gesendet oder als null gesendet“ zu vermeiden.