Große Zahlen in JSON: Warum IDs plötzlich falsch sind
Wenn eine ID in deinem JSON nach dem Parsen auf einmal andere letzte Ziffern hat, liegt das fast immer an der Zahlendarstellung deiner Programmiersprache, nicht am JSON selbst. JavaScript speichert jede Zahl als 64-Bit-Gleitkommazahl und kann ganze Zahlen nur bis 9007199254740991 exakt abbilden. Alles darüber wird gerundet. Das Problem ist als „json number precision“ bekannt und betrifft vor allem Datenbank-IDs, Snowflake-IDs und Zeitstempel in Nanosekunden.
Ob das JSON-Dokument selbst unversehrt ist, prüfst du im JSON-Formatierer. Er kopiert Zahlenliterale unverändert, rundet also nie. Alles läuft lokal im Browser, es wird nichts an einen Server gesendet.
Was passiert: ein Beispiel
const text = '{"id": 12345678901234567890}';
const daten = JSON.parse(text);
console.log(daten.id);
// 12345678901234567000
Der Text enthielt 12345678901234567890, nach JSON.parse steht dort 12345678901234567000. Es gibt keine Fehlermeldung. Schlimmer noch: Reichst du das Objekt mit JSON.stringify wieder weiter, steht die falsche Zahl im Ergebnis, und der Datensatz mit der echten ID wird nicht mehr gefunden.
Warum: IEEE-754-Double
JavaScript hat für gewöhnliche Zahlen nur einen Typ: den IEEE-754-Double (binary64). Er hat 53 Bit für die Genauigkeit. Daraus folgt:
- Bis
Number.MAX_SAFE_INTEGER, also9007199254740991(2^53 − 1), ist jede ganze Zahl exakt darstellbar. - Darüber fehlen Werte. Zwischen 2^53 und 2^54 sind nur noch gerade Zahlen darstellbar, danach nur noch Vielfache von 4 und so weiter.
Number.MAX_SAFE_INTEGER; // 9007199254740991
9007199254740992 === 9007199254740993; // true
Die Zahl 9007199254740993 landet beim Parsen also auf 9007199254740992. Mit Number.isSafeInteger(wert) prüfst du, ob ein Wert noch im sicheren Bereich liegt.
Was der Standard dazu sagt
JSON selbst legt keine Grenze für Zahlen fest. Die Grammatik erlaubt beliebig viele Ziffern. RFC 8259 weist in Abschnitt 6 aber ausdrücklich auf das Interoperabilitätsproblem hin: Viele Implementierungen nutzen IEEE-754-Double. Gute Interoperabilität sei dann gegeben, wenn Absender nicht mehr Genauigkeit oder Wertebereich erwarten, als dieses Format bietet.
Dein JSON ist also gültig, aber nicht überall verlustfrei lesbar. Welcher Parser eine große Zahl exakt behält, hängt von der Sprache ab. Python liest ganze Zahlen als beliebig große int, JavaScript rundet.
Das Vorbild: Twitter und id_str
Die Twitter-API (heute X) lieferte Tweet-IDs als 64-Bit-Zahlen. JavaScript-Clients lasen sie falsch. Deshalb enthalten die Antworten zusätzlich ein Feld id_str mit derselben ID als Text:
{
"id": 1234567890123456789,
"id_str": "1234567890123456789"
}
(Die Werte im Beispiel sind erfunden.) Das Muster hat sich bewährt: Wer die Zahl nie als Zahl, sondern als Text überträgt, verliert nichts.
Lösung 1: IDs als String übertragen
Die einfachste und robusteste Lösung. Eine ID ist ein Bezeichner, keine Größe, mit der du rechnest.
{ "id": "12345678901234567890" }
Das gehört in die API-Definition, sodass Sender und Empfänger es einheitlich handhaben. Du brauchst keine Sonderbehandlung im Parser, und der Wert bleibt in jeder Sprache identisch.
Lösung 2: BigInt mit Reviver
Kannst du das Format nicht ändern, kannst du die Zahl als BigInt lesen. Der Reviver von JSON.parse bekommt in neueren JavaScript-Engines als drittes Argument ein Objekt mit dem Originaltext (context.source):
const daten = JSON.parse(
'{"id": 12345678901234567890}',
(schluessel, wert, kontext) =>
typeof wert === "number" && !Number.isSafeInteger(wert)
? BigInt(kontext.source)
: wert
);
console.log(daten.id); // 12345678901234567890n
Beachte zwei Dinge:
- Die Unterstützung für
context.sourceist noch nicht in allen Browsern und Laufzeitumgebungen vorhanden. Prüfe sie vor dem Einsatz, zum Beispiel auf MDN oder caniuse. Das Beispiel funktioniert in aktuellen Node.js-Versionen. JSON.stringifywirft bei einemBigInteinenTypeError. Zum Schreiben brauchst du eine eigene Behandlung, etwa einen Replacer, der den Wert in einen String umwandelt.
Ohne diese Unterstützung helfen Bibliotheken wie json-bigint (npm). Sie parsen große Zahlen selbst und liefern BigInt oder eine Big-Number-Klasse zurück. Du ersetzt dann JSON.parse an der Stelle, an der die Antwort eingelesen wird.
Dezimalzahlen und Geld
Auch Nachkommastellen leiden unter dem Double-Format, nur anders: Viele Dezimalbrüche haben keine exakte binäre Darstellung.
0.1 + 0.2; // 0.30000000000000004
Das ist keine JSON-Eigenheit, sondern gilt in jeder Sprache mit Gleitkommazahlen. Für Geldbeträge bieten sich zwei Wege an:
{ "preis_cent": 1999 }
{ "preis": "19.99", "waehrung": "EUR" }
Ganze Cent-Beträge als Integer bleiben im sicheren Bereich, ein String mit Dezimalwert lässt sich im Zielsystem mit einem Decimal-Typ lesen. Rechne nie mit Gleitkommazahlen, wenn Rundungsfehler eine Rolle spielen.
Prüfen, wo die Zahl kaputtgeht
Wenn eine Zahl falsch ankommt, grenze die Stelle ein:
- Schau dir die Rohantwort an, zum Beispiel im Netzwerk-Tab. Steht dort noch die richtige Zahl, passiert der Fehler erst im Parser.
- Füge das JSON in den JSON-Formatierer ein. Beim Formatieren und Minifizieren bleibt jedes Zahlenliteral unverändert, ebenso die Reihenfolge der Schlüssel. Das Werkzeug ist also nicht die Ursache.
- Prüfe mit
Number.isSafeIntegerim Code, ob die Werte im sicheren Bereich liegen.
Anders beim Umwandeln: JSON in YAML umwandeln liest die Zahlen als JavaScript-Zahlen. Eine Zahl, die sich so nicht exakt speichern lässt, wird gerundet, und das Tool weist dich mit einem Hinweis darauf hin. Übertrage solche IDs deshalb als String.
Kurz gesagt
- JavaScript parst JSON-Zahlen als Double. Ganze Zahlen über
9007199254740991(Number.MAX_SAFE_INTEGER) werden gerundet, ohne Fehlermeldung. - Aus
12345678901234567890wird12345678901234567000. Das JSON ist gültig, der Verlust entsteht im Parser (RFC 8259, Abschnitt 6). - Übertrage IDs als String, wie es Twitter/X mit
id_strmacht. - Wenn du das Format nicht ändern kannst:
BigIntper Reviver mitcontext.source(Browser-Support prüfen) oder eine Bibliothek wiejson-bigint. - Für Geld: ganze Cent-Beträge oder Dezimalwerte als String, nicht
0.1 + 0.2.
Weiterführend: JSON-Fehler finden und beheben, Was ist JSON?, JSON minifizieren und der JSON-Formatierer.