Eine Anmerkung dazu, warum ein Proxy-Anbieter über „JSON.parse“ schreibt: Wir sind Geonode und verkaufen Proxys. Das häufigste „SyntaxError“, das unsere Kunden melden, hat überhaupt nichts mit JSON zu tun – es lautet „Unexpected token '<'“. Das bedeutet, dass die Antwort HTML statt JSON war, was wiederum bedeutet, dass die API eine Sperrseite, eine Weiterleitung zur Anmeldung oder eine Fehlerseite zurückgegeben hat. Hier also der ehrliche Hinweis: Wenn „JSON.parse“ bei den von Ihnen abgerufenen Daten ausgegeben wird, protokollieren Sie den rohen Antworttext, bevor Sie irgendetwas anderes ändern. In neun von zehn Fällen funktioniert der Parser einwandfrei und meldet korrekt, dass Ihnen eine Webseite gesendet wurde. Der Kauf von Proxys behebt dieses Problem nur in dem speziellen Fall, in dem Sie gesperrt wurden; bei einer falschen URL, einem abgelaufenen Token oder einer Ratenbegrenzung, die Sie einhalten sollten, hilft das nicht weiter. Geben Sie die Antwort zunächst aus.
Nachdem das geklärt ist, kommen wir nun zur eigentlichen API.
Die Grundlagen und die Fehler, auf die Sie tatsächlich stoßen werden
const data = JSON.parse('{"name": "Ada", "born": 1815}');
// { name: "Ada", born: 1815 }
Zwei Parameter: der Text und eine optionale „reviver“-Funktion. Das ist alles.
Es wird ein „SyntaxError
“ ausgelöst, wenn die Eingabe gegen die JSON-Grammatik verstößt, und die JSON-Grammatik ist in einigen Punkten strenger als die Syntax von JavaScript-Objektliteralen – was viele Nutzer überrascht. Vier Fälle decken nahezu alle Fehler ab.
Einfache Anführungszeichen. MDN sagt es ganz klar: „JSON-Zeichenketten müssen durch doppelte (nicht einfache) Anführungszeichen begrenzt werden.“ Gültiges JavaScript, ungültiges JSON.
JSON.parse("{'name': 'Ada'}"); // SyntaxError
JSON.parse('{"name": "Ada"}'); // fine
Abschließende Kommas. In modernem JavaScript zulässig, in JSON unzulässig:
JSON.parse("[1, 2, 3, 4, ]"); // SyntaxError
Nicht in Anführungszeichen gesetzte Schlüssel. {name: "Ada"}
ist ein gültiges Objektliteral, aber kein JSON. Schlüssel müssen in Anführungszeichen gesetzte Zeichenfolgen sein.
Die Antwort war kein JSON. Wie oben beschrieben. Unexpected token '<'
bedeutet, dass der Body mit <
begann, was auf HTML hindeutet. Unexpected end of JSON input
bedeutet in der Regel einen leeren Body – einen 204-Status, eine abgeschnittene Antwort oder einen Fetch, auf dessen Abschluss Sie nicht gewartet haben.
Wickeln Sie es immer ein und protokollieren Sie, was Sie tatsächlich erhalten haben:
function parseOrThrow(text, url) {
try {
return JSON.parse(text);
} catch (err) {
throw new Error(
`Failed to parse JSON from ${url}: ${err.message}. ` +
`First 200 chars: ${text.slice(0, 200)}`
);
}
}
Der „text.slice(0, 200)
“ ist der entscheidende Teil. Ein bloßes „SyntaxError
“ zeigt an, dass das Parsen fehlgeschlagen ist; die ersten 200 Zeichen geben Aufschluss über den Grund, und meist ist die Antwort sofort ersichtlich.
Die „Reviver“-Funktion und wozu sie dient
Das zweite Argument wandelt Werte während der Auswertung um:
const data = JSON.parse(text, (key, value) => {
if (key === "created") return new Date(value);
return value;
});
Drei Verhaltensweisen, die man genau kennen sollte.
Die Auswertung erfolgt in der Tiefe. Verschachtelte Eigenschaften werden vor ihren übergeordneten Eigenschaften abgearbeitet, und der letzte Aufruf verwendet eine leere Zeichenkette als Schlüssel für den Stammwert. Wenn Ihr Reviver also ein Objekt sieht, sind dessen untergeordnete Elemente bereits durchlaufen worden.
**Die Rückgabe von „undefined
“ löscht die Eigenschaft.** MDN: „Wenn die Funktion „reviver
“ „undefined
“ zurückgibt (oder keinen Wert zurückgibt), wird die Eigenschaft aus dem Objekt gelöscht.“ Dies lässt sich leicht versehentlich auslösen – ein Reviver mit einer bedingten Verzweigung, die über das Ende hinausgeht, gibt „undefined
“ zurück und entfernt Schlüssel stillschweigend. Geben Sie als Standard immer explizit „value
“ zurück.
Die Wurzel kann vollständig ersetzt werden. „Wenn Sie einen anderen Wert als „reviver
“ zurückgeben, ersetzt dieser Wert den ursprünglich geparsten Wert vollständig. Dies gilt sogar für den Wurzelwert.“
Die klassische Anwendung ist die Wiederherstellung von Datumsangaben, da JSON keinen Datums-Typ hat:
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
const data = JSON.parse(text, (key, value) =>
typeof value === "string" && ISO_DATE.test(value)
? new Date(value)
: value
);
Seien Sie vorsichtig beim Musterabgleich. Ein Reviver, der alles konvertiert, was wie ein Datum aussieht, wandelt auch Zeichenfolgen um, die Sie eigentlich als Zeichenfolgen beibehalten wollten – Versionsnummern, Identifikatoren, Benutzerinhalte, die zufällig wie ein Zeitstempel aussehen. Bevorzugen Sie den Abgleich anhand des Schlüsselnamens, wenn Sie das Schema kennen.
Beachten Sie außerdem den Aufwand: Der Reviver wird einmal pro Wert im Dokument aufgerufen. Bei großen Payloads ist dies ein echter Performance-Faktor, und oft ist es effizienter, die Daten zunächst einfach zu parsen und anschließend nur die Felder zu transformieren, die für Sie von Bedeutung sind.
Zugriff auf den Quelltext: context.source und JSON.rawJSON
Dies ist die wichtigste Neuerung der letzten Zeit, die vielen Entwicklern noch nicht bekannt ist.
Der Vorschlag zum Zugriff auf den Quelltext von JSON.parse hat Stufe 4 des TC39-Prozesses erreicht, was bedeutet, dass er für den Standard genehmigt ist. Er löst ein Problem, das im Vorschlag direkt angesprochen wird: „Die Umwandlung zwischen ECMAScript-Werten und JSON-Text ist verlustbehaftet.“
Der „reviver“ erhält nun ein drittes Argument für primitive Werte. MDN beschreibt „context.source
“ als „die ursprüngliche JSON-Zeichenkette, die diesen Wert darstellt“ – und der Vorschlag ist präziser: Er bezeichnet den Quelltext als „einschließlich Interpunktion, jedoch ohne führende oder nachgestellte unbedeutende Leerzeichen“, neben „index
“, „input
“ und „keys
“.
Warum dies wichtig ist, wird anhand eines Beispiels deutlich:
const text = '{"id": 9007199254740993}';
JSON.parse(text).id;
// 9007199254740992 — wrong, silently
JSON.parse(text, (key, value, context) =>
key === "id" ? BigInt(context.source) : value
).id;
// 9007199254740993n — correct
Ohne Zugriff auf den Quelltext wurde die Zahl bereits in einen JavaScript-Double-Wert umgewandelt, bevor Ihr Reviver sie sieht. Die Genauigkeit ist verloren, bevor Sie eingreifen können. context.source
liefert Ihnen die ursprünglichen Ziffern.
Der Vorschlag fügt außerdem „JSON.rawJSON()
“ hinzu, womit Sie rohen JSON-Text bereitstellen können, den „JSON.stringify
“ unverändert ausgibt – wodurch der Rundlauf vervollständigt wird, sodass ein aus JSON gelesenes BigInt ohne Verfälschung zurückgeschrieben werden kann.
JSON.stringify({ id: JSON.rawJSON("9007199254740993") });
// '{"id":9007199254740993}'
Prüfen Sie die Unterstützung für Ihre Zielumgebungen, bevor Sie sich darauf verlassen, aber dies ist nun die richtige Lösung für das Problem mit großen Zahlen und kein Workaround mehr.
Zahlenpräzision – der Fehler, den Sie ausliefern werden
Dieses Thema verdient einen eigenen Abschnitt, da es stillschweigend fehlschlägt und die Symptome weit entfernt von der Ursache auftreten.
JSON-Zahlen werden zu JavaScript-Zahlen, bei denen es sich um IEEE-754-Doubles handelt. Ganzzahlen oberhalb von Number.MAX_SAFE_INTEGER – 9.007.199.254.740.991 – können nicht alle exakt dargestellt werden. MDN bringt es auf den Punkt: Zahlen „können dabei an Genauigkeit verlieren“.
Das Gefährliche daran ist, dass kein Fehler ausgelöst wird. Man erhält eine Zahl. Es ist einfach nicht die Zahl, die gesendet wurde.
JSON.parse('{"id": 12345678901234567890}').id;
// 12345678901234567000
Wo dies in der Praxis Probleme verursacht:
- Datenbank-Identifikatoren. 64-Bit-Ganzzahl-Primärschlüssel überschreiten den sicheren Bereich. Zwei unterschiedliche Datensätze können zu derselben JavaScript-Zahl geparst werden.
- IDs im Snowflake-Stil. Werden von mehreren großen Plattformen verwendet und liegen regelmäßig über dem Grenzwert.
- Finanzbeträge in kleinen Einheiten. Große Summen in Cent oder Satoshis.
- Zeitstempel in Nanosekunden. Jeder Nanosekunden-Epochenwert seit 1970 liegt bereits außerhalb des sicheren Bereichs.
Drei Abhilfemaßnahmen, geordnet nach Präferenz:
Strings anfordern. Wenn Sie die Kontrolle über die API haben, serialisieren Sie große Identifikatoren als Strings. Dies ist die sauberste Lösung, sie funktioniert überall und kostet nichts. MDN empfiehlt genau dies: „Eine Möglichkeit, große Zahlen ohne Präzisionsverlust zu übertragen, besteht darin, sie als Zeichenketten zu serialisieren und anschließend wieder in BigInts umzuwandeln.“
Verwenden Sie „context.source“ mit einem Reviver. Wie oben beschrieben, wenn Sie keinen Einfluss auf den Erzeuger haben und Ihre Umgebung dies unterstützt.
Verwenden Sie eine JSON-Bibliothek mit BigInt-Unterstützung. Für ältere Umgebungen gibt es mehrere Parser, die dies bewältigen. Dies erfordert eine zusätzliche Abhängigkeit und geht mit gewissen Leistungseinbußen einher.
Was nicht funktioniert: Nach dem Parsen zu prüfen, ob die Zahl „richtig aussieht“. Zu diesem Zeitpunkt sind die Informationen bereits verloren, und ein falscher Wert ist von einem richtigen nicht mehr zu unterscheiden.
„__proto__“ und Prototyp-Verunreinigung
MDN nennt den einzigen Fall, in dem sich JSON und JavaScript in ihrer Bedeutung unterscheiden: „Der einzige Fall, in dem ein JSON-Text einen anderen Wert darstellt als derselbe JavaScript-Ausdruck, ist der Umgang mit dem Schlüssel ‚\"__proto__\"‘.“
In einem JavaScript-Objektliteral legt „__proto__“ den Prototyp fest. In „JSON.parse“ wird damit eine gewöhnliche eigene Eigenschaft erstellt: „
const fromLiteral = { __proto__: { admin: true } };
fromLiteral.admin; // true — prototype was set
const fromJson = JSON.parse('{"__proto__": {"admin": true}}');
fromJson.admin; // undefined — plain own property
Object.hasOwn(fromJson, "__proto__"); // true
```“
„`JSON.parse`“ selbst ist hier also unbedenklich – dies ist ein beabsichtigtes und korrektes Verhalten.
Die Gefahr liegt darin, was als Nächstes geschieht. Sicherheitslücken durch Prototyp-Verschmutzung treten fast immer dann auf, wenn geparste Daten durch Code, der gefährliche Schlüssel nicht filtert, in ein anderes Objekt zusammengeführt werden:
```javascript
// Unsafe: a naive deep merge can walk into Object.prototype
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === "object") {
merge(target[key] ?? (target[key] = {}), source[key]);
} else {
target[key] = source[key];
}
}
}
Führt man eine Payload ein, die „__proto__“ enthält, kann man „Object.prototype“ für das gesamte Programm verändern. Abwehrmaßnahmen:
Filtert die gefährlichen Schlüssel explizit heraus – __proto__, constructor, prototype – bei jeder Zusammenführung oder Zuweisung, die mit nicht vertrauenswürdigen Daten zu tun hat.
Verwendet „Object.create(null)“ für Objekte, die nicht vertrauenswürdige Schlüssel enthalten, damit kein Prototyp verunreinigt werden kann.
Verwenden Sie „Map“, wenn Sie tatsächlich einen Schlüssel-Wert-Speicher und kein strukturiertes Objekt erstellen.
Prüfen Sie die Daten anhand eines Schemas. Das ist die allgemeine Antwort, mit der sich auch andere Probleme erkennen lassen. Das Parsen und die Validierung sind separate Schritte, die beide notwendig sind.
JSON.parse im Vergleich zu eval im Vergleich zu Response.json()
Verwenden Sie niemals eval. Es führt beliebigen Code aus, ist für diesen Zweck langsamer und akzeptiert auch Daten, die kein JSON sind. Es gibt keinen Fall, in dem eval das richtige Werkzeug zum Parsen von JSON ist.
Response.json() ist genau das, was Sie brauchen, wenn Sie mit fetch arbeiten. Es liest den Body und parst ihn in einem Schritt:
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
Die Überprüfung mit res.ok ist der Teil, den viele überspringen, und genau das ist die direkte Ursache für den Fehler „Unexpected token '<'“ aus der Einleitung. fetch lehnt HTTP-Fehlerstatus nicht ab – ein 403 oder ein 500 wird normal verarbeitet, und anschließend versucht .json(), eine Fehlerseite zu parsen. Überprüfen Sie zuerst den Status, und sehen Sie sich content-type an, wenn Sie besonders gründlich vorgehen möchten:
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
const type = res.headers.get("content-type") ?? "";
if (!type.includes("application/json")) {
const body = await res.text();
throw new Error(`Expected JSON, got ${type}: ${body.slice(0, 200)}`);
}
const data = await res.json();
Beachten Sie, dass Response.json() keinen „Reviver“ akzeptiert. Wenn Sie einen benötigen, verwenden Sie res.text() gefolgt von JSON.parse.
Auch hier unterscheiden sich die Bibliotheken – einige analysieren automatisch und lösen bei Nicht-2xx-Statussen einen Fehler aus, was Auswirkungen darauf hat, wo Ihre Fehlerbehandlung angesiedelt sein sollte. Wir haben das Verhalten in axios vs. fetch verglichen.
Großes JSON-Dokument parsen, ohne dass die Seite einfriert
JSON.parse läuft synchron und blockierend ab. Wird ein großes Dokument im Hauptthread geparst, friert die Benutzeroberfläche für die Dauer des Vorgangs ein – dies ist eine häufige und leicht zu diagnostizierende Ursache für Ruckler.
Grobe Richtlinie: Bei weniger als einem Megabyte brauchst du dir keine Gedanken zu machen. Zwischen einem und zehn: Messen Sie die Leistung auf Ihrem langsamsten Zielgerät. Über zehn: Machen Sie etwas anderes.
Die Optionen, in aufsteigender Reihenfolge des Aufwands:
Verlagern Sie die Verarbeitung in einen Web Worker. Die einfachste echte Lösung. Führen Sie das Parsen außerhalb des Hauptthreads durch und senden Sie das Ergebnis zurück. Beachten Sie, dass die Übertragung des Ergebnisses eigene Kosten für den strukturierten Klon verursacht; daher ist dies am hilfreichsten, wenn der Worker auch die nachfolgende Verarbeitung übernimmt.
Fordern Sie weniger Daten an. Paginierung, Feldauswahl, ein engerer Endpunkt. Fast immer die richtige Lösung und fast immer wird sie übersprungen, da sie die Rücksprache mit dem API-Betreiber erfordert.
Verwende einen Streaming-Parser. Es gibt Bibliotheken, die Werte sofort bei ihrem Eintreffen ausgeben, anstatt den gesamten Baum aufzubauen. Das lohnt sich, wenn Dokumente wirklich groß sind oder wenn du nur einen Teil der Nutzdaten benötigst.
Verwende durch Zeilenumbrüche getrennte JSON-Daten. Bei großen Datensätzen lässt sich ein JSON-Dokument pro Zeile wesentlich einfacher schrittweise verarbeiten, da jede Zeile unabhängig geparst wird und ein abgeschnittener Stream dennoch vollständige Datensätze liefert. Wenn Sie das Format kontrollieren können, ist dies häufig die bessere Lösung – und die Vor- und Nachteile gegenüber anderen Formaten werden in unserem Vergleich von JSON und CSV behandelt.
Wann „JSON.parse“ das falsche Werkzeug ist
Wenn die Eingabe kein JSON ist. JSON5, JSONC und Konfigurationsdateien mit Kommentaren und nachgestellten Kommas erfordern jeweils eigene Parser. „JSON.parse“ lehnt diese zu Recht ab, und Sie sollten nicht versuchen, Kommentare mit einem regulären Ausdruck zu entfernen – dieser Weg führt zu einem Parser, den Sie gar nicht schreiben wollten.
Wenn Sie eine Validierung benötigen, nicht nur ein Parsing. Ein erfolgreiches Parsing zeigt Ihnen, dass die Syntax gültig war. Es sagt nichts darüber aus, ob die erforderlichen Felder vorhanden sind oder die richtigen Typen haben. Zuerst parsen, dann validieren; eine Schema-Validierungsbibliothek ist das richtige Werkzeug für den zweiten Schritt, try/catch hingegen nicht.
Wenn die Daten verlustfrei hin- und herübertragen werden müssen. Große Ganzzahlen, Datumsangaben, undefined, Funktionen, Map, Set, NaN, Infinity – nichts davon übersteht JSON unbeschadet. Wenn eine verlustfreie Hin- und Herübertragung erforderlich ist, verwende entweder bewusst context.source und JSON.rawJSON oder nutze ein dafür entwickeltes Format.
Wenn Sie bei jedem Rendering parsen. Das wiederholte Parsen derselben Zeichenkette in einem Hot Path ist reine Verschwendung. Parsen Sie einmal und speichern Sie das Ergebnis im Cache.
Wenn die Zeichenkette aus einem Abruf stammt, den Sie nicht überprüft haben. Der Punkt, mit dem wir begonnen haben, noch einmal wiederholt, da er der häufigste von allen ist. Wenn bei abgerufenen Daten ein Fehler „JSON.parse“ auftritt, liegt der Fehler stromaufwärts. Überprüfen Sie den Statuscode, überprüfen Sie den Inhaltstyp, protokollieren Sie den Body. Der Parser sagt Ihnen die Wahrheit.
Häufig gestellte Fragen
Was macht JSON.parse?
Es wandelt eine im JSON-Format vorliegende Zeichenkette in einen JavaScript-Wert um – Objekt, Array, Zeichenkette, Zahl, Boolescher Wert oder null. Es akzeptiert eine optionale Reviver-Funktion, die jeden Wert während des Parsens transformieren kann. Es löst eine „SyntaxError“-Ausnahme aus, wenn die Eingabe kein gültiges JSON ist.
Warum gibt JSON.parse die Fehlermeldung „Unexpected token '<'“ aus?
Weil die Zeichenkette mit „<“ beginnt, was bedeutet, dass Sie HTML statt JSON erhalten haben – in der Regel eine Fehlerseite, eine Weiterleitung nach der Anmeldung oder eine Sperrseite. Der Parser ist korrekt; das Problem liegt bei der Anfrage. Protokollieren Sie die ersten 200 Zeichen des Antwortkörpers, dann ist die Ursache normalerweise offensichtlich.
Wie parse ich JSON mit großen Zahlen in JavaScript?
Verwenden Sie das Argument „context.source“ des Revivers, um die ursprünglichen Ziffern zu lesen und ein „BigInt“ zu erstellen, da der Wert bereits an Genauigkeit verloren hat, wenn er Ihren Reviver erreicht. Besser noch: Wenn Sie die API kontrollieren, serialisieren Sie große Identifikatoren als Zeichenketten.
Was ist die „reviver“-Funktion in JSON.parse?
Ein optionales zweites Argument, das für jedes Schlüssel-Wert-Paar aufgerufen wird – in der Tiefe zuerst –, und das mit der Wurzel unter einem Schlüssel mit der leeren Zeichenkette endet. Was auch immer es zurückgibt, ersetzt den Wert; die Rückgabe von „undefined“ löscht die Eigenschaft. Seine übliche Aufgabe ist die Konvertierung von Zeichenketten in komplexere Typen wie „Date“.
Ist JSON.parse sicher?
Was die Ausführung von Code angeht, ja – im Gegensatz zu eval führt es niemals etwas aus. Es behandelt auch __proto__ sicher, indem es eine einfache eigene Eigenschaft erstellt, anstatt den Prototyp zu setzen. Das Risiko liegt darin, was Sie anschließend tun: Das Zusammenführen von nicht vertrauenswürdigen, geparsten Daten mit anderen Objekten ohne Filterung von __proto__ und constructor führt zu Prototyp-Verschmutzung.
Was ist der Unterschied zwischen JSON.parse und Response.json()?
Response.json() liest den Body einer Fetch-Antwort und parst ihn in einem Schritt; es akzeptiert keinen Reviver. JSON.parse verarbeitet eine Zeichenkette, die Sie bereits haben. Beachten Sie, dass fetch bei HTTP-Fehlern nicht abbricht. Überprüfen Sie daher res.ok, bevor Sie .json() aufrufen, da Sie sonst eine Fehlerseite parsen.
Kann JSON.parse Kommentare oder nachgestellte Kommas verarbeiten?
Nein. Beides ist ungültiges JSON und löst eine „SyntaxError“ aus. Wenn Ihre Eingabe diese enthält, handelt es sich um JSON5 oder JSONC, und es wird ein Parser für dieses Format benötigt, anstatt eines regulären Ausdrucks, der diese entfernt.
Blockiert „JSON.parse“ den Hauptthread?
Ja, die Funktion ist synchron. Bei Dokumenten unter einem Megabyte ist dies unerheblich; bei großen Datenmengen führt dies jedoch zu spürbaren Verzögerungen. Verlagern Sie das Parsen in einen Web Worker, fordern Sie weniger Daten an oder verwenden Sie einen Streaming-Parser.
Fazit: „
JSON.parse“ hat eine Signatur mit zwei Parametern und verbirgt eine überraschend große Tiefe. Die Aspekte, die es sich zu beachten lohnt, sind diejenigen, die eher still als laut fehlschlagen.
Am schwerwiegendsten ist die Zahlenpräzision: Große Ganzzahlen werden stillschweigend verfälscht, es wird kein Auswurf ausgelöst, und der falsche Wert ist vom richtigen nicht zu unterscheiden, bis etwas weiter unten in der Kette fehlschlägt. Die Lösung besteht entweder in string-kodierten Bezeichnern im Quellcode oder im Argument „context.source“ des Revivers, das sich mittlerweile in Phase 4 befindet und Teil des Standards ist.
Der „reviver“ verdient mehr Beachtung, als er bekommt, insbesondere bei Datumsangaben – mit dem Vorbehalt, dass das Versäumen, „value“ auf dem Standardpfad zurückzugeben, Eigenschaften stillschweigend löscht. Und die Prototyp-Verunreinigung ist überhaupt kein Problem von „JSON.parse“ – die Funktion verarbeitet „__proto__“ korrekt –, sondern ein Problem für das, was das Ergebnis zusammenführt, was jedoch nah genug dran ist, um eine Rolle zu spielen.
Alles andere lässt sich auf eine Gewohnheit zurückführen: Wenn das Parsen der abgerufenen Daten fehlschlägt, protokolliere den Rohtext, bevor du irgendetwas am Code änderst. Die Fehlermeldung enthält fast immer die Antwort, und meistens ist es so, dass du gar kein JSON erhalten hast.