Ein kurzer Hinweis dazu, warum ein Proxy-Anbieter dies schreibt: Wir sind Geonode, und der mit Abstand häufigste JSON-Fehler, den unsere Kunden melden, ist „Unexpected token '<'“ bei den von ihnen abgerufenen Daten. Das bedeutet, dass die Antwort HTML war – eine Fehlerseite, eine Weiterleitung zur Anmeldung oder eine Sperrseite – und der Parser korrekt meldet, dass kein JSON bereitgestellt wurde. Bevor Sie irgendwelche Änderungen am Code vornehmen, protokollieren Sie die ersten 200 Zeichen der empfangenen Daten. Fast alle weiteren Ausführungen in diesem Artikel gehen davon aus, dass es sich bei der Datei tatsächlich um JSON handelt, und genau diese Annahme ist es, die am häufigsten fehlschlägt.
In Node: Vom Datenträger lesen
Drei Ansätze, von denen einer die moderne Lösung darstellt.
** „fs/promises
“, der Standardweg:**
import { readFile } from "node:fs/promises";
const raw = await readFile("./data.json", "utf8");
const data = JSON.parse(raw);
Die Node-Dokumentation geht konkret auf das „encoding“-Argument ein, und das ist wichtig: Ohne eine Kodierung gibt readFile
„ein Promise zurück, das mit einem <Buffer>
-Objekt erfüllt wird, das den Dateiinhalt enthält“; mit einer Kodierung „wird es mit einem <string>
erfüllt“. JSON.parse
akzeptiert einen Buffer, indem es ihn in eine Zeichenkette umwandelt; daher funktioniert das Weglassen der Kodierung in der Regel und führt zu einer unnötigen zusätzlichen Konvertierung. Übergeben Sie "utf8"
.
Es unterstützt außerdem eine AbortSignal
über die Option signal
, „die es Ihnen ermöglicht, einen laufenden readFile
-Vorgang abzubrechen“ – nützlich, wenn ein Lesevorgang Teil einer Anfrage ist, die möglicherweise abgebrochen wird.
Synchron, für Startcode:
import { readFileSync } from "node:fs";
const config = JSON.parse(readFileSync("./config.json", "utf8"));
Blockieren ist in Ordnung, bevor Ihr Server mit der Bedienung beginnt. Innerhalb eines Request-Handlers ist es jedoch nicht in Ordnung, da es dort die Ereignisschleife für jede andere Verbindung blockiert. Diese Unterscheidung ist die eigentliche Regel.
** „require
“, nur in CommonJS:**
const data = require("./data.json");
Prägnant und mit zwei Eigenschaften, die oft vergessen werden. Es wird zwischengespeichert, sodass ein zweiter Aufruf von ``require`
` mit demselben Pfad dasselbe Objekt zurückgibt, ohne die Datei erneut einzulesen – was bedeutet, dass eine Bearbeitung der Datei zur Laufzeit keine Auswirkungen hat. Außerdem ist es in ES-Modulen nicht verfügbar.
Attribute importieren: Die Standardmethode
Die Syntax, auf die die meisten noch nicht umgestiegen sind und die man in neuem Code verwenden sollte.
import data from "./data.json" with { type: "json" };
Oder dynamisch:
const data = await import("./data.json", { with: { type: "json" } });
MDN führt dies als Baseline 2025 auf, verfügbar seit April 2025 in den neuesten Browsern, wobei Nicht-Browser-Laufzeiten wie Node und Deno sich an die Browsersemantik für JSON-Module anpassen.
Das Attribut „type: "json"
“ ist keine bloße Dekoration. MDN erklärt, dass es „überprüft, ob ein Modul mit dem MIME-Typ application/json
bereitgestellt wird“, und dass der Import fehlschlägt, „wenn die Datei mit einem anderen Medientyp als application/json
bereitgestellt wird“.
Die sicherheitstechnische Begründung ist es wert, zitiert zu werden, da sie erklärt, warum das Attribut obligatorisch und nicht optional ist:
Wenn aus irgendeinem Grund (z. B. weil der Server gekapert wurde oder gefälscht ist) der Medientyp in der Serverantwort auf „
text/javascript
“ (für JavaScript-Quellcode) gesetzt ist, würde die Datei als Code geparst und ausgeführt werden. Wenn die „JSON“-Datei tatsächlich bösartigen Code enthält, würde die Deklaration „import
“ unbeabsichtigt externen Code ausführen, was eine ernsthafte Bedrohung darstellt.
Ein Hinweis zur Migration: Ein früherer Vorschlag verwendete das Schlüsselwort „assert
“ anstelle von „with
“. MDN stuft dies als „breaking change“ ein – Implementierungen, die „assert
“ verwenden, „werden nicht mehr unterstützt“. Wenn Sie „assert { type: "json" }
“ in älterem Code oder einem Tutorial finden, muss dies aktualisiert werden.
Im Browser: JSON abrufen
Der häufigste Fall – und der, der eine Tücken birgt.
const res = await fetch("/data.json");
if (!res.ok) throw new Error(`HTTP ${res.status} from ${res.url}`);
const data = await res.json();
Die Überprüfung „res.ok“ ist nicht optional, und das Überspringen dieser Überprüfung ist die direkte Ursache für den Fehler in der Einleitung dieses Artikels. „fetch“ lehnt HTTP-Fehlerstatus nicht ab – ein 403, ein 404 und ein 500 werden alle normal verarbeitet. Der Aufruf von .json() versucht dann, eine Fehlerseite zu parsen, und Sie erhalten einen Syntaxfehler bezüglich eines Zeichens „<“, das nichts mit Ihrem JSON zu tun hat.
Für eine Version, die aussagekräftig fehlschlägt:
async function fetchJson(url) {
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)}`);
}
return res.json();
}
Zwei Zeilen zur Überprüfung wandeln einen undurchsichtigen Parsing-Fehler in eine Meldung um, die den Status, den Inhaltstyp und den tatsächlich eingegangenen Inhalt nennt.
Beachten Sie außerdem, dass Response.json() keine Reviver-Funktion akzeptiert. Wenn Sie eine benötigen – beispielsweise für die Datumsumwandlung oder die Verarbeitung großer Ganzzahlen –, verwenden Sie res.text() gefolgt von JSON.parse. Wir haben Reviver und das Präzisionsproblem in unserem Leitfaden zu JSON.parse behandelt.
Im Browser: Eine vom Benutzer ausgewählte Datei
Verwenden Sie für eine Datei, die vom Computer des Benutzers ausgewählt wurde, die File-API.
<input type="file" id="picker" accept="application/json">
document.getElementById("picker").addEventListener("change", async e => {
const file = e.target.files[0];
if (!file) return;
try {
const data = JSON.parse(await file.text());
console.log(data);
} catch (err) {
console.error(`Could not parse ${file.name}: ${err.message}`);
}
});
File.text()
gibt ein Promise zurück, das den Inhalt als String liefert, was wesentlich übersichtlicher ist als das ältere FileReader
mit seinen Ereignis-Handlern. FileReader
ist nach wie vor die richtige Wahl, wenn Sie Fortschrittsereignisse für eine sehr große Datei benötigen.
Zwei Dinge sind zu beachten: Browser können keine beliebigen lokalen Pfade lesen – der Benutzer muss die Datei auswählen, und dies ist eine bewusste Sicherheitsgrenze und keine Einschränkung, die man umgehen sollte. Und eine „.json
“-Erweiterung garantiert nichts über den Inhalt, sodass „try
“ bzw. „catch
“ die eigentliche Arbeit erledigt.
Drag-and-Drop verwendet dieselben „File
“-Objekte, die über „event.dataTransfer.files
“ abgerufen werden.
Fehlerbehandlung, die Ihnen etwas sagt
Die Gewohnheit, die den Unterschied zwischen einem fünfminütigen und einem einstündigen Problem ausmacht.
function parseJson(text, source) {
try {
return JSON.parse(text);
} catch (err) {
throw new Error(
`Failed to parse JSON from ${source}: ${err.message}. ` +
`First 200 chars: ${text.slice(0, 200)}`
);
}
}
Auf den Ausschnitt kommt es an. JSON.parse Fehler nennen eine Position und ein Zeichen; die tatsächliche Eingabe verrät Ihnen den Grund. Drei Signaturen decken die meisten Fälle ab:
Unexpected token '<' — Der Inhalt ist HTML. Eine Fehlerseite, eine Weiterleitung nach der Anmeldung oder eine Verzeichnisauflistung.
Unexpected end of JSON input — Die Eingabe ist leer oder unvollständig. Eine 204-Antwort, eine Datei, die nicht vollständig geschrieben werden konnte, oder ein Abruf, auf dessen Abschluss du vergessen hast zu warten.
Unexpected token '}' an einer plausiblen Stelle – tatsächlich fehlerhaftes JSON, häufig ein abschließendes Komma. JSON verbietet diese, auch wenn JavaScript sie zulässt.
Unterscheiden Sie beim Lesen von Dateien zwischen einem Lesefehler und einem Parsing-Fehler. „ENOENT“ bedeutet, dass die Datei nicht existiert, was ein anderes Problem als ungültige Inhalte darstellt und eine andere Meldung verdient.
Überprüfung des Gelesenen
Die Analyse war erfolgreich. Das sagt Ihnen lediglich, dass die Syntax gültig war, sagt aber nichts darüber aus, ob die Daten auch tatsächlich die Form haben, die Ihr Code erwartet – und genau in dieser Lücke liegt ein überraschend großer Teil der Produktionsfehler.
Parsen und Validierung sind getrennte Schritte. JSON.parse
gibt problemlos „{"user": {"nmae": "Ada"}}
“ mit einem Tippfehler im Schlüssel zurück oder ein Feld „price
“, das die Zeichenfolge „"n/a"
“ enthält, obwohl eine Zahl erwartet wurde. Ihr Code schlägt dann irgendwo weiter unten fehl, mehrere Funktionen entfernt vom eigentlichen Problem, mit einem Fehler, der eher ein Symptom als eine Ursache benennt.
Bei allem, was außerhalb Ihrer Kontrolle liegt, sollten Sie anhand eines Schemas validieren. Mehrere Bibliotheken bewältigen dies gut, und das Muster ist unabhängig davon, für welche Sie sich entscheiden, immer dasselbe:
const Config = z.object({
port: z.number().int().min(1).max(65535),
host: z.string(),
retries: z.number().int().default(3),
features: z.array(z.string()).optional(),
});
const config = Config.parse(JSON.parse(await readFile("./config.json", "utf8")));
Der Fehler nennt nun das Feld, den erwarteten Typ und das, was gefunden wurde – was den Unterschied zwischen einer fünfminütigen Korrektur und einem ganzen Nachmittag ausmacht.
In einfachen Fällen kosten ein paar Assertions nichts:
const data = JSON.parse(raw);
if (!Array.isArray(data.items)) throw new Error("items must be an array");
if (data.items.length === 0) throw new Error("items is empty — check the source");
Diese zweite Überprüfung ist wertvoller, als es den Anschein hat. Ein leeres Array ist gültiges JSON, lässt sich problemlos parsen und ist sehr oft eher ein Symptom als ein legitimes Ergebnis – beispielsweise eine API, die nichts zurückgegeben hat, weil ein Filter falsch war, oder ein Scrape, der bei einer Seite erfolgreich war, die sich inzwischen geändert hatte.
Seien Sie skeptisch gegenüber Zahlen, die Sie nicht selbst erzeugt haben. JSON-Zahlen werden zu JavaScript-Double-Werten, sodass Ganzzahlen über Number.MAX_SAFE_INTEGER
stillschweigend an Genauigkeit verlieren, ohne dass an irgendeiner Stelle ein Fehler auftritt. Identifikatoren sind die üblichen Opfer: Zwei unterschiedliche Datenbankdatensätze können beim Parsen denselben Wert ergeben. Wenn ein Feld eher ein Identifikator als eine Menge ist, sollte es im JSON eine Zeichenkette sein – und wenn Sie keinen Einfluss auf den Erzeuger haben, liefert Ihnen das Argument „context.source
“ des Revivers die ursprünglichen Ziffern.
Und validieren Sie an der Schnittstelle – einmalig. Wenn Sie die Struktur der Daten beim Eintritt in Ihr Programm überprüfen, kann alles nachgelagerte System davon ausgehen, dass sie korrekt sind. Eine defensive Überprüfung an zwanzig Stellen bedeutet zwanzig Stellen, die aktualisiert werden müssen, und keinen einzigen Punkt, an dem die Vereinbarung festgehalten ist.
Große Dateien: Die
JSON.parse
-Funktion arbeitet synchron und benötigt das gesamte Dokument im Arbeitsspeicher. Beides wird zu einem Problem, wenn die Dateien größer werden.
Grobe Richtlinie: Bei weniger als einem Megabyte brauchen Sie sich keine Gedanken zu machen. Bei Dateien zwischen einem und zehn Megabyte sollten Sie die Situation prüfen – insbesondere im Hauptthread eines Browsers, wo das Parsen das Rendering blockiert und sichtbare Ruckler verursacht. Bei mehr als zehn Megabyte oder etwa einem Zehntel Ihres verfügbaren Speichers sollten Sie eine andere Lösung wählen.
Verlagern Sie die Verarbeitung aus dem Hauptthread. In einem Browser parst ein Web Worker, ohne die Benutzeroberfläche einzufrieren. In Node übernimmt ein Worker-Thread dieselbe Aufgabe für die Ereignisschleife.
Verwenden Sie durch Zeilenumbrüche getrennte JSON-Daten. Dies ist eher eine strukturelle Lösung als ein Workaround. Ein JSON-Dokument pro Zeile bedeutet, dass Sie eine Datei beliebiger Größe mit konstantem Speicherbedarf verarbeiten, jede Zeile unabhängig geparst wird und eine unvollständige Datei dennoch jeden vollständigen Datensatz liefert:
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
const rl = createInterface({ input: createReadStream("./data.jsonl") });
for await (const line of rl) {
if (line.trim()) handle(JSON.parse(line));
}
Wenn Sie das Format kontrollieren, ist dies das bessere Design für alles, was im Laufe der Zeit angehängt wird – und der Grund, warum ein abgestürzter Erfassungsjob eine nutzbare Datei hinterlässt statt einer nicht parsbaren.
Verwenden Sie einen Streaming-Parser, wenn das Format ein einzelnes großes Array ist, das Sie nicht ändern können. Mehrere Bibliotheken geben Werte sofort bei ihrem Eintreffen aus, anstatt den gesamten Baum aufzubauen.
Oder fordern Sie weniger an. Paginierung, Feldauswahl, ein enger gefasster Endpunkt. Fast immer die richtige Antwort und fast immer übersehen, weil dafür ein Gespräch mit dem Verantwortlichen für die API erforderlich ist.
JSON zurückschreiben
Kurz gesagt das Spiegelbild, da dies meist die nächste Frage ist.
import { writeFile } from "node:fs/promises";
await writeFile("./out.json", JSON.stringify(data, null, 2), "utf8");
Die Argumente „null, 2
“ erzeugen eine eingerückte Ausgabe, was wichtiger ist, als es scheint: Eine Datei, die jemals von einem Menschen gelesen oder in der Versionskontrolle verglichen wird, sollte formatiert sein, während eine Datei, die übertragen wird, nicht formatiert sein sollte.
Drei Dinge, die den Aufruf von ``JSON.stringify`
nicht überstehen und eher zu stillschweigendem Datenverlust als zu Fehlern führen:undefined`
-Werte und -Funktionen werden aus Objekten vollständig entfernt und werden innerhalb von Arrays zu ``null`
. Date`
-Objekte werden zu ISO-Zeichenketten, sodass sie ohne einen Reviver nicht wieder in Datumsangaben umgewandelt werden können. Und BigInt
löst direkt einen Fehler aus – der Standardansatz besteht darin, große Ganzzahlen als Zeichenketten zu serialisieren.
Zum Anhängen sollten Sie JSON mit Zeilenumbrüchen schreiben, anstatt ein Array neu zu schreiben:
import { appendFile } from "node:fs/promises";
await appendFile("./log.jsonl", JSON.stringify(record) + "\n", "utf8");
Das Anhängen an ein JSON-Array erfordert das Einlesen, Parsen, Hinzufügen und Neuschreiben der gesamten Datei – was ressourcenintensiv ist und die Datei beschädigt, falls der Vorgang während des Schreibvorgangs abbricht.
Häufig gestellte Fragen
Wie lese ich eine JSON-Datei in Node.js ein?
const data = JSON.parse(await readFile("./data.json", "utf8")) unter Verwendung von node:fs/promises. Oder verwenden Sie die Standard-Import-Syntax: import data from "./data.json" with { type: "json" }, die ab 2025 zum Standard gehört und sowohl in Node als auch in Browsern funktioniert.
Kann JavaScript eine lokale Datei im Browser lesen?
Nicht über den Pfad. Browser können keine beliebigen lokalen Dateien öffnen – dies ist eine bewusste Sicherheitsbeschränkung. Der Nutzer muss eine Datei über ein „<input type="file">“ oder per Drag-and-Drop auswählen; anschließend liefert File.text() den Inhalt.
Was bewirkt „import ... with { type: "json" }“?
Es importiert eine JSON-Datei als Modul und überprüft dabei, ob der Server sie mit dem MIME-Typ „application/json“ bereitgestellt hat. Ohne diese Überprüfung würde eine als „text/javascript“ bereitgestellte Datei als Code geparst und ausgeführt werden – genau dieses Sicherheitsproblem soll das Attribut verhindern.
Warum erhalte ich beim Einlesen von JSON die Meldung „Unexpected token '<'“?
Weil der Inhalt mit < beginnt, was bedeutet, dass Sie HTML statt JSON erhalten haben – in der Regel eine Fehlerseite oder eine Weiterleitung zur Anmeldung. Bei fetch liegt die Ursache fast immer in einer fehlenden Überprüfung auf res.ok, da fetch bei HTTP-Fehlerstatus nicht zurückweist.
Sollte ich für JSON „require“ oder „import“ verwenden?
Verwenden Sie in neuem Code „import ... with { type: "json" }“, da dies der Standard ist und in ES-Modulen funktioniert. „require“ ist nur für CommonJS vorgesehen und speichert das Ergebnis im Cache, sodass eine zur Laufzeit bearbeitete Datei nicht erneut gelesen wird. Beides ist ungeeignet für Dateien, die sich während der Ausführung Ihres Programms ändern – verwenden Sie hierfür „readFile“.
Wie lese ich eine sehr große JSON-Datei ein?
Verlagern Sie das Parsen mit einem Worker aus dem Hauptthread oder strukturieren Sie die Daten als durch Zeilenumbrüche getrennte JSON-Daten um, sodass jede Zeile unabhängig voneinander mit konstantem Speicherbedarf geparst wird. Verwenden Sie für ein unveränderliches großes Array einen Streaming-Parser. Und überlegen Sie, ob Sie von vornherein weniger Daten anfordern können.
Was ist der Unterschied zwischen JSON.parse und response.json()?
Response.json() liest einen Fetch-Body und parst ihn in einem Schritt; es akzeptiert keine Reviver-Funktion. JSON.parse arbeitet mit einer bereits vorhandenen Zeichenkette und akzeptiert eine Reviver-Funktion. Wenn Sie einen Reviver benötigen – beispielsweise für Datumsangaben oder große Ganzzahlen –, verwenden Sie res.text(), gefolgt von JSON.parse.
Wie gehe ich mit einer JSON-Datei um, die möglicherweise nicht existiert?
Fangen Sie den Lesefehler getrennt vom Parsing-Fehler ab. In Node bedeutet der Fehlercode „ENOENT“, dass die Datei fehlt, was in der Regel einen Standardwert anstelle eines Fehlers erfordert – während „SyntaxError“ bedeutet, dass die Datei existiert, ihr Inhalt jedoch falsch ist.
Fazit
Die Vorgehensweise hängt von der Umgebung ab, und die moderne Lösung ist einheitlicher als früher. „import data from "./data.json" with { type: "json" }“ funktioniert sowohl in Node als auch in Browsern, gilt ab 2025 als Baseline und beinhaltet eine MIME-Typ-Prüfung, die aus echten Sicherheitsgründen und nicht nur als reine Formalität dient.
Bei Dateien, die sich während der Ausführung Ihres Programms ändern, lesen Sie diese explizit ein – readFile mit einer "utf8"-Kodierung in Node, fetch mit einer res.ok-Prüfung im Browser und File.text() für vom Benutzer ausgewählte Dateien. Diese res.ok-Prüfung ist die wertvollste Zeile in diesem Artikel, da das Überspringen dieser Prüfung die direkte Ursache für den häufigsten JSON-Fehler überhaupt ist.
Wenn etwas fehlschlägt, protokollieren Sie die ersten 200 Zeichen der Eingabe, bevor Sie irgendwelchen Code anfassen. „Unexpected token '<'“ bedeutet HTML, „Unexpected end of JSON input“ bedeutet leer oder abgeschnitten, und beide werden beantwortet, indem man sich ansieht, was tatsächlich angekommen ist, anstatt über den Parser zu spekulieren.
Und wenn die Dateien zu groß werden, ist die strukturelle Lösung JSON mit Zeilenumbrüchen statt einer leistungsstärkeren Maschine. Ein Dokument pro Zeile wird mit konstantem Speicherbedarf gestreamt, lässt sich sicher anhängen und übersteht einen unterbrochenen Schreibvorgang, wobei jeder vollständige Datensatz intakt bleibt.
