Stejný JSON, čtyři nástroje, čtyři výsledky: jak vybrat formátovač
Shrnutí: Formátování není bezeztrátové. Stejný JSON s úvodní nulou, BOM nebo 20místným celým číslem dává v Pythonu, Node, jq a json.tool různé výsledky: někde chyba, jinde tichá změna hodnoty. Deset vlastních vzorků na čtyřech cestách dává srovnávací tabulku a kopírovatelný skript přísné kontroly.

Závěr: Nejzásadnější rozdíl mezi JSON formátovači není počet mezer odsazení, ale to, že stejný vstup u jednoho nástroje skončí chybou a u jiného tiše změní data. Než si vyberete, prožeňte nástrojem jeden vzorek s hraniční hodnotou — řekne vám víc než celý seznam funkcí.
Formátování lidé považují za „přerovnání textu". Ve skutečnosti každá cesta znovu interpretuje vstup svým vlastním parserem. Když je parser benevolentní, chyba, která měla být vyhozena, se změní na tichou úpravu hodnoty — a stránka přitom dál svítí uklidňující zelenou hláškou „Formátováno úspěšně".
1. Jevení: jeden vstup, čtyři cesty, čtyři výsledky
Testováno na Python 3.11.1, Node v22.13.1 a jq 1.7. Deset ručně sestavených krátkých JSON řetězců šlo přes standardní vstup, bez zápisu na disk a bez změny konfigurace. Cesty: json.loads, JSON.parse, jq -c ., python -m json.tool. ERR znamená výjimku a přerušení, SILENT znamená bez chyby, ale výstup se liší od vstupu.
| Vzorek | 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 exact | SILENT 9007199254740992 | OK exact | 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 |
Čtyři z deseti vzorků daly „bez chyby, ale jiná data": S4, S6, S7, S8. Kdo věří jen zelené hlášce, má se čtyřicetiprocentní pravděpodobností v rukou už ne původní data. Záludné je, že žádný z těchto případů není v seznamu syntaktických chyb — soubor jde otevřít, zkopírovat, exportovat, chyba je v obsahu, ne v syntaxi.
2. Příčina: syntaktická a sémantická vrstva jsou dvě odlišné věci
RFC 8259 to říká otevřeně: oddíl 1.2 doporučuje vyhnout se praktikám, které ECMA-404 povoluje, kvůli maximální interoperabilitě; oddíl 4 vyžaduje jedinečné názvy objektů jen jako SHOULD, bez určení, jak naložit s duplicitními klíči; oddíl 6 u čísel dává jen doporučení pro interoperabilitu. ECMA-404 jde dál a výslovně se prohlašuje za „agnostické vůči sémantice čísel" — řídí jen zápis číselné sekvence, ne uložení v jazyce. Specifikace tedy ponechává implementacím široké hraniční pásmo.
„Formátování" se tak rozpadá na dvě vrstvy: syntaktická rozhoduje, zda je text legální, sémantická rozhoduje, jaká hodnota z něj vznikne. První má standardní odpověď, druhá ne. Že jq čte 01 jako 1 a přeskočí úvodní BOM, je volba sémantické vrstvy; že Node natlačí 20místné celé číslo do IEEE 754 double, je volba runtime; že všechny čtyři cesty u {"a":1,"a":2} ponechají poslední hodnotu, je společná konvence velkých parserů. Nic z toho neporušuje „parsovatelné", ale všechno mění data.
Jak opravit chyby parsování JSON pracuje na syntaktické vrstvě a překládá text chyby na souřadnice. Tento článek řeší krok před ním: když nástroj neohlásí vůbec žádnou chybu, nedostanete ani souřadnice.
3. Čtyři tiché změny dat, jedna po druhé
Vyjmeme-li čtyři tiché vzorky samostatně, jsou změny tak malé, že je při zběžném přečtení přehlédnete:
{"a": 01} // leading zero: jq emits {"a":1}
\ufeff{"a": 1} // BOM: jq skips the first three bytes
{"id": 9007199254740993} // 20-digit int: JSON.parse gives ...992
{"a": 1, "a": 2} // duplicate key: all four keep {"a":2}
Úvodní nula a BOM patří k typu „vstup už byl neplatný, ale nástroj byl shovívavý". 01 je ve standardním JSON nelegální (RFC 8259 oddíl 6), BOM nepatří na začátek JSON textu; Python i Node obojí odmítnou, jq pokračuje. Při čištění logů je to pohodlné, při kontrole kontraktu fatální: upstream si myslí, že poslal chybná data k opravě, zatímco downstream už drží „normálně vypadající" výsledek.
Velké celé číslo je typ „vstup zcela legální, ale runtime ho neunese". 9007199254740993 je legální JSON číslo, leží ale nad 2^53-1; JSON.parse umí jen 9007199254740992. Python int i jq zachovají původní hodnotu. Jde-li o číslo objednávky nebo Snowflake ID, jedna změněná poslední číslice znamená, že se neshoduje účtování.
Duplicitní klíče jsou typ „specifikace mlčí, implementace si vybere". Všechny čtyři cesty tiše vezmou poslední hodnotu, bez jediného varování. RFC 8259 říká jen SHOULD, takže to není bug — ale při spojování konfigurací nebo merge patchů je to nejobtížněji dohledatelná třída problémů.
4. Schopnost lokalizovat chybu: kdo dá řádek a sloupec, kdo jen jednu větu
I u šesti neúspěšných vzorků se množství informací výrazně liší. Pythonův JSONDecodeError dává zároveň msg, lineno, colno a pos; Node v22 hlásí position N (line 1 column M) plus úsek zdroje u chyby; python -m json.tool využívá informace Pythonu; jq dává jen parse error: ... at line N bez znakového offsetu.
U jednořádkového minifikovaného JSON jsou řádek a sloupec k ničemu — vše je na řádku 1. Potřebujete znakový offset. Následující skript sjednotí kontrolu na „chyba + 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())
Je přísnější než jq . (odmítne úvodní nulu a BOM) a dává e.pos. Jak převést line 1 column 23611 na bajtový offset a proč se znakový a bajtový offset v CJK dokumentech liší trojnásobně, je rozebráno v Ladění minifikovaného JSON, zde to neopakujeme.
5. Oprava: rozdělte formátování na „striktní kontrolu" + „zesličnění"
Závěr je prostý: jedno tlačítko „Formátovat" nesmí zároveň nést kontrolu i zkrášlení. Nejprve striktním parserem určete legalitu, poté zvolte nástroj pro úpravu vzhledu. První krok oddělíte jedním příkazem:
python3 -m json.tool input.json > /dev/null && echo "syntax OK" || echo "invalid syntax"
python3 -c "import json,sys; d=json.load(open(sys.argv[1],encoding='utf-8')); print(type(d).__name__, len(d))" input.json
První řádka odpoví jen na „jde to parsovat?", druhá prověří „je výsledná struktura ta očekávaná?". Jsou-li v datech velká celá čísla, čtěte Pythonem nebo jq, ne JSON.parse; může-li zdroj obsahovat BOM, čtěte explicitně s encoding='utf-8-sig' a nespoléhejte na automatické přeskočení; mohou-li se klíče opakovat, proskenujte vstup před parsováním zvlášť, protože žádná ze čtyř cest nevaruje. Zapsáno do CI je to přesně pipeline z Editory, příkazový řádek a automatizační nástrojové řetězce, jen s přidanou vrstvou asercí datové věrnosti.
6. Jak vybrat: srovnávací tabulka čtyř cest
| Cesta | Síla | Riziko | Vhodné pro |
|---|---|---|---|
python json.loads / json.tool |
Striktní, dává řádek/sloupec a znakový offset, celá čísla bez ztráty | Odmítá úvodní nulu a BOM, kódování nutné řešit explicitně | Kontrola kontraktů, CI, analýza logů |
node JSON.parse |
Nativní v prohlížeči, chyba obsahuje úsek zdroje | Velká celá čísla tiše ořízne, BOM způsobí chybu | Frontend debugging, nástroje v prohlížeči |
jq -c . |
Přátelské k rourám, nejrychlejší výsledek | Úvodní nula a BOM projdou tiše, bez znakového offsetu | Provozní pipelines, rychlé vytáhnutí pole |
Prohlížečové nástroje /parse, /editor |
Stromový pohled, řazení klíčů, kopírování cesty, bez instalace | Chování závisí na parseru stránky, nutno jednou ověřit | Ruční prohlížení, ad-hoc hledání chyb |
Rozhodující otázka není „kdo má víc funkcí", ale „lhaje tento nástroj o mých datech?". Pro čísla objednávek, částky nebo ID zařízení preferujte Python nebo nástroj s výslovně deklarovanou datovou věrností; pro čisté ladění struktury bez uložení stačí prohlížeč. /parse na tomto webu kontroluje a formátuje v prohlížeči — ideální pro „nejdřív se podívat", ale u velkých celých čísel raději překontrolujte výše uvedeným skriptem.
7. Soukromí je jen jedna z hranic
„Nahrají se moje data?" je u online nástrojů nejčastější otázka, ale je nezávislá na „změní se moje data?". Lokální offline nástroj klidně ořízne 20místné celé číslo; naopak online nástroj se striktními chybami může původní text poslat POSTem na server. Obě vlastnosti je třeba ověřit zvlášť, jedna druhou nenahradí. FAQ o soukromí dává tři dvouminutové kontroly; tento článek doplňuje druhou polovinu — se seznamem vzorků níže potvrďte, že nástroj tiše nemění hodnoty.
8. Kopírovatelný kontrolní seznam
Uložte S1–S10 jako cases.txt, jeden vzorek na řádek, vložte do každého kandidáta a ověřte tři věci: selhaly chybové vzorky opravdu? Uvedla chyba konkrétní pozici? Je výstup úspěšného vzorku znak po znaku shodný se vstupem? Zvlášť zkontrolujte S4, S6, S7, S8 — stačí jediný řádek SILENT a daná cesta není použitelná samostatně k validaci dat.
Přidejte tři technická pravidla: striktní kontrolu do pre-commit hooku, ať nesrovnalost celých čísel neobjevíte až v produkci; soubory napříč jazyky zapisujte jako UTF-8 bez BOM; u JSON od třetích stran uchovejte syrové tělo odpovědi a formátovaný výsledek používejte jen ke čtení, nikdy ke zpětnému zápisu.
Časté otázky
Znamená úspěšné formátování, že je JSON v pořádku?
Ne. S4, S6, S7 a S8 všechny „prošly" formátováním, ale úvodní nula, BOM, 20místné celé číslo a duplicitní klíč byly přepsány nebo pohlceny. Formátování dokazuje jen, že nějaký parser text přijal, ne že hodnoty odpovídají odeslaným. Kritická pole ověřujte hodnotu po hodnotě.
Proč jq přijímá úvodní nulu a BOM, které Python odmítá?
jq má vlastní lexer, u čísel jako 01 je benevolentní a přeskočí úvodní BOM; Python a Node se drží RFC 8259 striktně. Není to chyba verze, ale rozdíl mezi tolerantním a striktním parserem. Při čištění první šetří práci, při kontrole kontraktu je druhý povinný.
Jak zabráním, aby se velké celé číslo změnilo na 9007199254740992?
Number v JavaScriptu je IEEE 754 double s bezpečnou horní hranicí 2^53-1. Čtení 20místného ID přes JSON.parse ho nevyhnutelně ořízne. Ukládejte taková ID jako řetězec nebo použijte Python int, jq či parser s BigInt. Potřebuje-li frontend opravdu číslo, nahraďte pole řetězcem ještě před parsováním.
Online nástroj, nebo lokální příkazová řádka — co zvolit?
Rozdělte podle citlivosti a účelu: jen prohlédnout strukturu či najít pole → prohlížeč je nejrychlejší; do pipeline a s asercemi datové věrnosti → python -m json.tool plus striktní kontrola. V obou případech nejdřív spusťte S1–S10, porovnejte chyby a výstupy s očekáváním, a teprve poté rozhodněte, jakou třídu dat dovnitř pustíte.