Startseite / Artikel / Was JSON.stringify leise weglässt, konvertiert und zur Serialisierung verweigert

Was JSON.stringify leise weglässt, konvertiert und zur Serialisierung verweigert

Erfahren Sie, welche JavaScript-Werte von JSON.stringify weggelassen oder verändert werden, wie toJSON, Ersatzfunktionen und Wiederherstellungsfunktionen dies beheben sowie wann structuredClone das bessere Werkzeug ist.

1602 Wörter

JSON.stringify beschwert sich selten. Wenn es auf einen Wert stößt, den es nicht darstellen kann, überspringt es diesen in der Regel oder wandelt ihn in etwas anderes um und setzt die Verarbeitung fort, wodurch ein vollkommen gültiges JSON zurückgegeben wird. So treten Fehler oft mehrere Schritte entfernt von ihrer Ursache auf: Die Daten verschwinden während der Serialisierung, doch das Problem zeigt sich erst später im Code, der überhaupt nicht weiß, dass eine Serialisierung stattgefunden hat. Diese Anleitung listet auf, was verloren geht, was seinen Typ ändert und was Fehler auslöst, und zeigt anschließend die eingebauten Werkzeuge (toJSON, Ersatzfunktionen, Wiederherstellungsfunktionen sowie structuredClone), mit denen Sie jeden Fall gezielt steuern können.

Eigenschaften, die spurlos verschwinden

Betrachten wir ein Session-Objekt, das eine Mischung aus gewöhnlichen Daten, einem Callback, einem explizit undefinierten Feld und einem Symbol enthält:

const session = {
  userId: 42,
  role: "admin",
  onExpire: () => console.log("session expired"),
  lastActivity: undefined,
  tempToken: Symbol("temp"),
};

console.log(JSON.stringify(session));
// {"userId":42,"role":"admin"}

Die Ausgabe enthält nur zwei der fünf Eigenschaften. onExpire, lastActivity und tempToken fehlen völlig – es gibt weder Ausnahmen noch Warnungen, und in der resultierenden Zeichenkette ist nichts zu finden, das darauf hindeutet, dass etwas entfernt wurde. Innerhalb eines Objekts überspringt JSON.stringify alle Eigenschaften, deren Wert undefined, eine Funktion oder ein Symbol ist. Keiner dieser Werte hat eine Darstellung im JSON-Format, und anstatt fehlzuschlagen, gibt der Serialisierer weiterhin gültiges JSON für das Verbleibende zurück.

Die Folge ist subtil. Ein Verbraucher, der diese Zeichenkette parsen und erwarten muss, dass lastActivity vorhanden ist – selbst mit einem undefined-Wert – wird es nicht finden. Der Schlüssel ist nicht leer; er ist von Anfang an nie in der Ausgabe aufgetaucht. Code, der zwischen „fehlend“ und „vorhanden, aber undefined“ unterscheidet – beispielsweise mit „lastActivity“ in obj oder Object.keys – wird sich nun auf der anderen Seite anders verhalten.

Arrays verhalten sich unterschiedlich

Dieselben nicht unterstützten Werte erhalten innerhalb eines Arrays eine andere Behandlung:

console.log(JSON.stringify([undefined, function () {}, 1]));
// [null,null,1]

Hier werden sie zu null, anstatt einfach zu verschwinden. Ein Array kann kein Element entfernen, ohne die Indizes aller nachfolgenden Elemente zu verschieben; daher behält der Serialisierer den Platz bei und füllt ihn mit dem Wert, der im JSON am ehesten „Nichts“ darstellt. Die Ursache ist identisch, doch es treten je nachdem, ob der Wert in einem Objekt oder in einem Array lag, zwei unterschiedliche, stille Verhaltensweisen auf. Zwei weitere verwandte Randfälle folgen demselben Prinzip: NaN und Infinity werden als null serialisiert, und die direkte Aufrufung von JSON.stringify auf undefined oder einer Funktion gibt stattdessen undefined statt einer Zeichenkette zurück.

Daten kommen als Zeichenketten zurück

Daten scheinen eine Rückreise zu überstehen, aber nur teilweise:

const record = { createdAt: new Date() };
const json = JSON.stringify(record);
console.log(json); // {"createdAt":"2026-09-07T14:30:00.000Z"}

const restored = JSON.parse(json);
console.log(restored.createdAt instanceof Date); // false
console.log(typeof restored.createdAt);          // "string"

Die Datenelemente selbst sind unverändert; sie erscheinen direkt im Ausgabeergebnis als ISO 8601-String. Was verloren geht, ist die Typinformation. JSON.parse kann nicht erkennen, dass ein bestimmter String ursprünglich ein Date-Objekt war und nicht nur wie solches aussieht, weshalb es einen String zurückgibt, der weiterhin als String bleibt, es sei denn, etwas wandelt ihn wieder um.

Das ist von Bedeutung, sobald der Code nach dieser Übertragung eine Methode für das Datum aufruft. Ausdrücke wie record.createdAt.getFullYear() werfen Fehler aus – nicht, weil die Daten falsch sind, sondern weil ihr Typ im Laufe des Prozesses stillschweigend geändert wurde. Dies tritt häufig bei API-Antworten, aus localStorage wiederhergestellten Werten sowie über Queues übertragenen Nachrichten auf.

Zyklische Strukturen verursachen stattdessen Fehler

Nicht jeder Fehler bleibt unauffällig. Einige Objekte können überhaupt nicht serialisiert werden, und der Interpreter macht das deutlich hörbar bekannt:

const parent = { name: "parent" };
const child = { name: "child", parent };
parent.child = child;

JSON.stringify(parent); // TypeError: Converting circular structure to JSON

Zur Erstellung der Ausgabe durchläuft JSON.stringify das Objektgraphen. Wenn ein Objekt im Graphen auf eines seiner Vorfahren verweist, würde diese Durchlaufprozedur niemals enden. Anstatt unbegrenzt rekursiv vorzugehen, erkennt der Encoder den Zyklus und wirft sofort einen TypeError aus. Dies ist einer der wenigen Fälle, in denen der Serialisierer ehrlich versagt – und das aus gutem Grund: Es gibt keine teilweisen oder annähernden Lösungen, da ein zyklischer Graph tatsächlich nicht in einen JSON- Baum umgewandelt werden kann. BigInt-Werte sind ein weiteres Beispiel; sie verursachen ebenfalls einen TypeError, es sei denn, man konvertiert sie selbst.

Die Ausgabe mit toJSON steuern

Einige eingebauten Typen bestimmen bereits selbst, wie sie als JSON aussehen sollen, weshalb ein Date-Objekt zu einem ISO-String wird anstelle eines leeren Objekts. Jedes Objekt kann dieses Verhalten durch Definition einer toJSON-Methode übernehmen. Wenn eine solche Methode vorhanden ist, ruft JSON.stringify sie auf und serialisiert ihren Rückgabewert anstelle der eigenen Eigenschaften des Objekts:

class Money {
  constructor(cents) {
    this.cents = cents;
  }
  toJSON() {
    return { amount: this.cents / 100, currency: "USD" };
  }
}

const price = new Money(3499);
console.log(JSON.stringify({ price })); // {"price":{"amount":34.99,"currency":"USD"}}

Ohne toJSON würde die Money-Instanz in ihrer internen Form, {"cents":3499}, gespeichert werden, wodurch Implementierungsdetails preisgegeben würden, auf die andere Teile des Systems niemals angewiesen sein sollten. Mit toJSON definiert das Objekt seine eigene öffentliche Darstellung. Date verwendet genau dieses Mechanismus: Date.prototype.toJSON erzeugt den ISO-String.

Beachten Sie, dass dies einseitig ist. Die Auswertung der Ausgabe liefert ein einfaches Objekt mit amount und currency, nicht eine Money-Instanz; das Wiederherstellen der Klasse ist die Aufgabe des im Folgenden beschriebenen Revivers.

Ersetzer und Reviver: Gestaltung in beide Richtungen

JSON.stringify akzeptiert ein optionales zweites Argument, eine Ersetzerfunktion, die für jede Schlüssel-Wert-Paarung vor dem Schreiben aufgerufen wird. Das Zurückgeben von undefined löscht den Eintrag, während das Zurückgeben eines anderen Wertes diesen ersetzt. Dadurch lässt sich auf saubere Weise auf sensible Felder beim Ausgeben zugreifen:

const user = { id: 1, name: "Priya", passwordHash: "a1b2c3..." };

const safe = JSON.stringify(user, (key, value) => {
  return key === "passwordHash" ? undefined : value;
});
console.log(safe); // {"id":1,"name":"Priya"}

JSON.parse bietet das Gegenstück: einen Reviver, der nach der Auswertung für jede Schlüssel-Wert-Paarung aufgerufen wird. Dies ist die offizielle Lösung für das zuvor gezeigte Date-Problem:

const restored = JSON.parse(json, (key, value) => {
  if (key === "createdAt") return new Date(value);
  return value;
});
console.log(restored.createdAt instanceof Date); // true

Zwei Punkte sind erwähnenswert. Reviver laufen von unten nach oben, sodass bereits eingebettete Werte wiederhergestellt werden, wenn ihr Elternelement verarbeitet wird. Zudem führt eine Überprüfung allein nach dem Schlüsselnamen zu einer Trefferfindung in jeder Tiefe – daher wird auch ein createdAt-Feld innerhalb eines eingebetteten Objekts umgewandelt. Falls das nicht gewünscht ist, muss auch die Formatierung des Wertes überprüft werden.

Niemand dieser Aspekte gehört zu den obskuren Ecken der API. Es handelt sich dabei um die vorgesehene Methode, um die Serialisierung präzise zu steuern, und sie ist weitaus zuverlässiger als das Löschen von Eigenschaften vor der Stringifizierung oder das manuelle Anpassen von Objekten nach der Parsung.

Map und Set verlieren alles

Entwickler, die erwarten, dass JSON jede Art von Objekt beibehält, geraten hier oft in Schwierigkeiten:

const tags = new Set(["urgent", "billing"]);
console.log(JSON.stringify({ tags })); // {"tags":{}}

Für den Serialisierer ist ein Set weder ein Array noch ein einfaches Objekt mit auflistbaren eigenen Eigenschaften, weshalb es zu einem leeren Objekt wird und alle darin enthaltenen Werte stillschweigend verworfen werden. Ein Map erleidet dasselbe Schicksal. Falls eines davon erhalten bleiben soll, muss es vor der Stringifizierung explizit umgewandelt werden – beispielsweise indem es in ein Array ausgebreitet wird:

const json = JSON.stringify({ tags: [...tags] }); // convert Set to array before stringifying

Für einen Map erzeugen [...map] oder Object.fromEntries(map) serialisierbare Formen, sodass ein Rekonstruktionsmechanismus die ursprüngliche Sammlung wiederherstellen kann.

Tiefe Kopien: Verwenden Sie structuredClone

Jahrelang war JSON.parse(JSON.stringify(obj)) eine gängige Methode, um ein Objekt tief zu klonen. Sie funktioniert nur zufällig und weist alle oben beschriebenen Einschränkungen auf: Funktionen sowie undefined-Werte verschwinden, Datumsangaben werden zu Strings, Sammlungen leeren sich, und zyklische Referenzen verursachen Fehler.

Moderne Browser und Node.js bieten eine eigens dafür entwickelte Alternative:

const clone = structuredClone(original);

structuredClone führt eine echte tiefe Kopie mithilfe des strukturierten Klon-Algorithmus durch. Er bewahrt Date, Map und Set bei und handhabt zyklische Referenzen – all das wird durch den JSON-Trick entweder verfälscht oder abgelehnt. Allerdings hat er auch eigene Einschränkungen: Bei Funktionen und DOM-Elementen wird ein DataCloneError ausgelöst, und Klasseninstanzen werden als einfache Objekte ohne ihr Prototyp zurückgegeben. Wenn das Ziel lediglich darin besteht, Daten zu kopieren, ist structuredClone fast immer die bessere Wahl. JSON.stringify wurde nie zum Klonen konzipiert; er war lediglich so praktisch, dass die Leute ihn dafür nutzten.

Haupterkenntnisse

  • JSON.stringify serialisiert nur den Teil der JavaScript-Werte, den JSON darstellen kann, und nicht beliebige JavaScript-Werte.
  • In Objekten werden undefined, Funktionen und Symbole weggelassen; in Arrays werden sie zu null.
  • Daten bleiben als ISO-Strings erhalten, verlieren aber ihre Typinformation; verwenden Sie einen Wiederhersteller, um sie zurückzusetzen.
  • Zyklische Referenzen sowie BigInt verursachen Fehler; Map und Set werden schweigend in {} serialisiert.
  • Verwenden Sie toJSON, um die öffentliche Struktur eines Objekts zu definieren, einen Ersatzfunktionen, um den Ausgabeinhalt zu filtern, sowie einen Wiederhersteller, um die Typen wiederherzustellen.
  • Wenden Sie structuredClone an, wenn Sie eine Kopie benötigen, und betrachten Sie JSON.stringify als Filter zur Formatierung, dessen Lücken Sie explizit behandeln müssen, damit Felder, Sammlungen und Typen nicht stillschweigend zwischen den Komponenten Ihres Systems verschwinden.
  • Verwandte Literatur

  • Was npm install wirklich macht: Registrierung, package.json und Lockfiles — Eine praktische Einführung in npm: das Registrierungssystem und die CLI, wie npm install Pakete auflöst, was package.json und package-lock.json speichern, sowie wie man veröffentlicht. —