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, also 9007199254740991 (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.source ist 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.stringify wirft bei einem BigInt einen TypeError. 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:

  1. Schau dir die Rohantwort an, zum Beispiel im Netzwerk-Tab. Steht dort noch die richtige Zahl, passiert der Fehler erst im Parser.
  2. 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.
  3. Prüfe mit Number.isSafeInteger im 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 12345678901234567890 wird 12345678901234567000. Das JSON ist gültig, der Verlust entsteht im Parser (RFC 8259, Abschnitt 6).
  • Übertrage IDs als String, wie es Twitter/X mit id_str macht.
  • Wenn du das Format nicht ändern kannst: BigInt per Reviver mit context.source (Browser-Support prüfen) oder eine Bibliothek wie json-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.