Dieselbe JSON-Datei, vier Tools, vier Ergebnisse: Wie wählt man einen Formatter?
Fazit: Formatieren ist nicht verlustfrei. Dieselbe JSON mit führender Null, BOM oder 20-stelliger Ganzzahl liefert bei Python, Node, jq und json.tool unterschiedliche Ergebnisse: Manche melden einen Fehler, andere ändern den Wert stillschweigend. Zehn selbst durchgeführte Stichproben über vier Pfade ergeben eine Auswahltabelle und ein kopierbares Prüfskript.

Fazit: Der entscheidende Unterschied zwischen JSON-Formattern ist nicht die Einrückung, sondern dass dieselbe Eingabe entweder einen Fehler meldet oder stillschweigend die Daten verändert. Ein einziger Grenzwert-Test sagt mehr als jede Funktionsliste.
Man hält Formatieren für „neu einrücken". Tatsächlich interpretiert jeder Pfad den Text mit seinem eigenen Parser neu. Ist der Parser nachsichtig, wird ein Fehler, der geworfen werden müsste, zur stillen Änderung eines Wertes — während die Seite weiterhin ein grünes „Erfolgreich formatiert" anzeigt.
1. Beobachtung: eine Eingabe, vier Pfade, vier Ergebnisse
Getestet mit Python 3.11.1, Node v22.13.1 und jq 1.7. Zehn kurze, von Hand gebaute JSON-Zeichenketten gingen über die Standardeingabe hinein, ohne auf die Platte zu fallen und ohne geänderte Konfiguration. Die Pfade: json.loads, JSON.parse, jq -c ., python -m json.tool. ERR bedeutet Abbruch mit Exception, SILENT bedeutet kein Fehler, aber die Ausgabe weicht von der Eingabe ab.
| Probe | python json.loads | node JSON.parse | jq -c . | python -m json.tool |
|---|---|---|---|---|
S1 {"a":1,"b":[1,2]} |
OK | OK | OK | OK |
S2 {"a":1,} |
ERR | ERR | ERR | ERR |
S3 {'a':1} |
ERR | ERR | ERR | ERR |
S4 {"a":01} |
ERR char 6 | ERR position 6 | SILENT {"a":1} |
ERR |
S5 {"a":1 /*x*/} |
ERR char 7 | ERR | ERR | ERR |
S6 \ufeff{"a":1} |
ERR utf-8-sig | ERR position 0 | SILENT {"a":1} |
ERR |
S7 {"id":9007199254740993} |
OK originaltreu | SILENT 9007199254740992 | OK originaltreu | OK |
S8 {"a":1,"a":2} |
SILENT {"a":2} |
SILENT {"a":2} |
SILENT {"a":2} |
OK |
S9 {"a":1 |
ERR char 6 | ERR | ERR EOF | ERR |
S10 <html>502</html> |
ERR char 0 | ERR Unexpected token '<' | ERR | ERR |
Vier von zehn Proben lieferten „kein Fehler, aber andere Daten": S4, S6, S7, S8. Wer nur der grünen Erfolgsmeldung traut, hält mit vierzig Prozent Wahrscheinlichkeit nicht mehr die ursprünglichen Daten. Das Tückische: Keiner dieser Fälle steht in einer Syntaxfehler-Liste. Die Datei lässt sich öffnen, kopieren, exportieren — der Fehler sitzt im Inhalt, nicht in der Syntax.
2. Ursache: Syntaxebene und Bedeutungsebene sind zweierlei
RFC 8259 sagt es deutlich: Abschnitt 1.2 empfiehlt, von ECMA-404 erlaubte Praktiken zugunsten maximaler Interoperabilität zu meiden; Abschnitt 4 verlangt eindeutige Objektnamen nur als SHOULD, ohne die Behandlung doppelter Schlüssel festzulegen; Abschnitt 6 gibt zu Zahlen lediglich Interoperabilitätsempfehlungen. ECMA-404 geht weiter und erklärt sich ausdrücklich für „agnostisch gegenüber der Semantik von Zahlen": Nur die Darstellung wird geregelt, nicht die Speicherung in der Sprache. Die Spezifikation überlässt den Implementierern also einen weiten Grenzbereich.
Damit zerfällt „Formatieren" in zwei Ebenen: Die Syntaxebene entscheidet, ob der Text legal ist, die Bedeutungsebene entscheidet, welcher Wert daraus wird. Erstere hat eine Standardantwort, Letztere nicht. Dass jq 01 als 1 liest und ein führendes BOM überspringt, ist eine Entscheidung der Bedeutungsebene; dass Node eine 20-stellige Ganzzahl in ein IEEE-754-Double presst, ist eine Entscheidung der Laufzeit; dass alle vier Pfade bei {"a":1,"a":2} den letzten Wert behalten, ist eine gemeinsame Konvention großer Parser. Keines verletzt „parsbar", aber alle verändern die Daten.
JSON-Parse-Fehler beheben arbeitet auf der Syntaxebene und übersetzt Fehlermeldungen in Koordinaten. Dieser Artikel behandelt den Schritt davor: Wenn das Tool gar keinen Fehler meldet, bekommt man nicht einmal Koordinaten.
3. Vier stille Änderungen im Einzelnen
Isoliert man die vier stillen Proben, sind die Änderungen so klein, dass man sie beim Überfliegen übersieht:
{"a": 01} // führende Null: jq gibt {"a":1} aus
\ufeff{"a": 1} // BOM: jq überspringt die ersten drei Bytes
{"id": 9007199254740993} // 20-stellige Zahl: JSON.parse gibt ...992 aus
{"a": 1, "a": 2} // doppelter Schlüssel: alle vier behalten {"a":2}
Führende Null und BOM gehören zum Typ „Eingabe war bereits ungültig, aber das Tool war nachsichtig". 01 ist in Standard-JSON illegal (RFC 8259 Abschnitt 6), ein BOM gehört nicht an den Anfang von JSON-Text. Python und Node lehnen beides ab, jq macht weiter. Beim Log-Cleaning ist das bequem, bei der Vertragsprüfung fatal: Upstream glaubt, ungültige Daten gesendet zu haben, die korrigiert werden müssen, während Downstream bereits ein „normal aussehendes" Ergebnis hält.
Die große Ganzzahl ist der Typ „Eingabe völlig legal, aber die Laufzeit kann sie nicht halten". 9007199254740993 ist eine legale JSON-Zahl, liegt aber über 2^53-1; JSON.parse kann nur 9007199254740992 darstellen. Pythons int und jq bewahren den Originalwert. Handelt es sich um eine Bestellnummer oder eine Snowflake-ID, bedeutet eine geänderte letzte Ziffer, dass die Abrechnung nicht mehr aufgeht.
Doppelte Schlüssel sind der Typ „Spezifikation schweigt, Implementierung wählt". Alle vier Pfade nehmen stillschweigend den letzten Wert, ohne jede Warnung. RFC 8259 sagt nur SHOULD, also ist es kein Bug — aber beim Zusammenfügen von Konfigurationen oder Mergen von Patches ist es die am schwersten auffindbare Klasse.
4. Fehlermeldungen: wer Koordinaten liefert, wer nur eine Zeile
Auch bei den sechs fehlschlagenden Proben differiert der Informationsgehalt stark. Pythons JSONDecodeError liefert msg, lineno, colno und pos zugleich; Node v22 meldet position N (line 1 column M) plus einen Ausschnitt der fehlerhaften Stelle; python -m json.tool nutzt Pythons Informationen; jq gibt nur parse error: ... at line N ohne Zeichenoffset.
Bei einzeilig minifiziertem JSON sind Zeile und Spalte praktisch wertlos — alles steht in Zeile 1. Gebraucht wird ein Zeichenoffset. Das folgende Skript normiert die Prüfung auf „Fehler + Offset + Kontext":
import json, sys
def strict_check(text: str) -> None:
try:
json.loads(text)
print("OK")
except json.JSONDecodeError as e:
print(f"ERR {e.msg} | char {e.pos} | line {e.lineno} col {e.colno}")
lo, hi = max(0, e.pos - 30), min(len(text), e.pos + 30)
print("...", text[lo:hi].replace("\n", "\\n"), "...")
sys.exit(1)
strict_check(open(sys.argv[1], encoding="utf-8").read())
Es ist strenger als jq . (lehnt führende Null und BOM ab) und liefert e.pos. Wie man line 1 column 23611 in einen Byte-Offset übersetzt und warum Zeichen- und Byte-Offsets in CJK-Dokumenten um den Faktor drei auseinanderliegen, steht in Minifiziertes JSON debuggen und wird hier nicht wiederholt.
5. Die Korrektur: Formatieren in „strikte Prüfung" + „Verschönerung" trennen
Die Lösung ist einfach: Ein „Formatieren"-Knopf darf nicht Prüfung und Verschönerung zugleich tragen. Erst mit einem strikten Parser die Legalität klären, dann die Darstellung wählen. Der erste Schritt lässt sich in einer Zeile abtrennen:
python3 -m json.tool input.json > /dev/null && echo "Syntax OK" || echo "Syntax ungueltig"
python3 -c "import json,sys; d=json.load(open(sys.argv[1],encoding='utf-8')); print(type(d).__name__, len(d))" input.json
Zeile eins beantwortet nur „parsbar?", Zeile zwei prüft „ist die Struktur die erwartete?". Bei großen Ganzzahlen mit Python oder jq statt JSON.parse lesen; bei möglichem BOM explizit encoding='utf-8-sig' angeben statt auf automatisches Überspringen zu hoffen; bei möglichen Doppelschlüsseln vor dem Parsen separat scannen, weil keiner der vier Pfade warnt. In die CI geschrieben ist das genau die Pipeline aus Editoren, Kommandozeile und Automatisierungs-Toolchains, nur mit einer zusätzlichen Schicht Datentreue-Assertions.
6. Auswahl: Vergleichstabelle der vier Pfade
| Pfad | Stärke | Risiko | Geeignet für |
|---|---|---|---|
python json.loads / json.tool |
Streng, liefert Zeile/Spalte und Zeichenoffset, Ganzzahlen bleiben exakt | Lehnt führende Null und BOM ab, Encoding muss explizit behandelt werden | Vertragsprüfung, CI, Log-Analyse |
node JSON.parse |
Nativ im Browser, Fehler enthält Quelltext-Ausschnitt | Große Ganzzahlen werden still gekappt, BOM führt zum Fehler | Frontend-Debugging, Browser-Tools |
jq -c . |
Pipeline-freundlich, schnellstes Ergebnis | Führende Null und BOM gehen still durch, kein Zeichenoffset | Betriebs-Pipelines, schnelles Feld-Extrahieren |
Browser-Tools /parse, /editor |
Baumansicht, Schlüsselsortierung, Pfad kopieren, keine Installation | Verhalten hängt vom Parser der Seite ab, daher einmal prüfen | Manuelle Sichtung, Ad-hoc-Fehlersuche |
Die Leitfrage ist nicht „wer hat mehr Funktionen", sondern „lügt dieses Tool über meine Daten?". Für Bestell-IDs, Beträge oder Geräte-IDs bevorzugt man Python oder ein Tool mit ausdrücklich erklärter Datentreue; für reines Struktur-Debugging ohne Speicherung genügt der Browser. /parse auf dieser Site prüft und formatiert im Browser — ideal für „erst einmal ansehen", aber bei großen Ganzzahlen bitte mit obigem Skript gegenprüfen.
7. Datenschutz ist nur eine der Grenzen
„Werden meine Daten hochgeladen?" ist die häufigste Frage zu Online-Tools, aber sie ist unabhängig von „werden meine Daten verändert?". Ein lokales, offline lauffähiges Tool kann trotzdem eine 20-stellige Ganzzahl kappen; umgekehrt kann ein streng fehlermeldendes Online-Tool den Originaltext an einen Server POSTen. Beides ist getrennt zu prüfen und ersetzt sich nicht gegenseitig. Die Datenschutz-FAQ nennt drei Prüfungen für zwei Minuten; dieser Artikel ergänzt die andere Hälfte: Mit der folgenden Probenliste bestätigen, dass das Tool keine Werte still verändert.
8. Eine kopierbare Selbstprüfliste
S1–S10 als cases.txt speichern, eine Zeile je Probe, in jedes Kandidaten-Tool einfügen und drei Dinge prüfen: Schlagen die Fehlerproben wirklich fehl? Nennt ein Fehler eine konkrete Position? Ist die Ausgabe einer erfolgreichen Probe zeichengleich mit der Eingabe? Besonders S4, S6, S7, S8 prüfen — eine einzige SILENT-Zeile bedeutet, dass dieser Pfad nicht allein zur Datenvalidierung taugt.
Dazu drei technische Regeln: strikte Prüfung in einen Pre-Commit-Hook legen, statt den Integer-Konflikt erst in Produktion zu entdecken; Dateien sprachübergreifend als UTF-8 ohne BOM schreiben; bei JSON von Dritten den rohen Response-Body aufbewahren und das formatierte Ergebnis nur zum Lesen, nie zum Zurückschreiben verwenden.
Häufige Fragen
Bedeutet ein erfolgreiches Formatieren, dass das JSON in Ordnung ist?
Nein. S4, S6, S7 und S8 haben alle „formatiert", doch führende Null, BOM, 20-stellige Ganzzahl und doppelter Schlüssel wurden verändert oder geschluckt. Formatieren beweist nur, dass irgendein Parser den Text akzeptiert hat — nicht, dass die Werte den gesendeten entsprechen. Kritische Felder einzeln als Wert assertions prüfen.
Warum akzeptiert jq führende Nullen und BOM, die Python ablehnt?
jq hat einen eigenen Lexer, ist bei Zahlen wie 01 nachsichtig und überspringt ein führendes BOM; Python und Node folgen strikt RFC 8259. Das ist kein Versionsfehler, sondern der Unterschied zwischen tolerantem und striktem Parser. Beim Bereinigen spart ersterer Arbeit, bei der Vertragsprüfung ist letzterer Pflicht.
Wie verhindere ich, dass eine große Ganzzahl zu 9007199254740992 wird?
JavaScripts Number ist ein IEEE-754-Double mit der sicheren Obergrenze 2^53-1. Eine 20-stellige ID mit JSON.parse zu lesen kappt sie zwangsläufig. Solche IDs als String speichern oder Pythons int, jq bzw. einen BigInt-fähigen Parser nutzen. Braucht das Frontend wirklich eine Zahl, das Feld vor dem Parsen durch einen String ersetzen.
Online-Tool oder lokale Kommandozeile — was soll ich nehmen?
Nach Sensibilität und Zweck trennen: Nur Struktur ansehen oder ein Feld finden → Browser am schnellsten; in die Pipeline und mit Datentreue-Assertions → python -m json.tool plus strikter Checker. In beiden Fällen erst S1–S10 laufen lassen, Fehler und Ausgaben gegen die Erwartung prüfen, dann entscheiden, welche Datenklasse man hineingibt.