JSON Toolkit

Ten sam JSON, cztery narzędzia, cztery wyniki: jak wybrać formater

Podsumowanie: Formatowanie nie jest bezstratne. Ten sam JSON z zerem wiodącym, BOM lub 20-cyfrową liczbą daje w Pythonie, Node, jq i json.tool różne wyniki: jedne zgłaszają błąd, inne po cichu zmieniają wartość. Dziesięć własnych próbek na czterech ścieżkach daje tabelę wyboru i gotowy skrypt ścisłej walidacji.

Intermediate~6 min czytaniaOpublikowano 2026-11-03Zaktualizowano 2026-11-03
Cztery ścieżki analizy zwracają różne wyniki dla tego samego JSON

Wniosek: najważniejsza różnica między formaterami JSON nie dotyczy liczby spacji wcięcia, lecz tego, że ten sam tekst w jednym narzędziu kończy się błędem, a w innym po cichu zmienia dane. Zanim wybierzesz, przepuść przez narzędzie jedną próbkę z wartością graniczną — powie Ci więcej niż cała lista funkcji.

Formatowanie uchodzi za „przeformatowanie tekstu". W rzeczywistości każda ścieżka interpretuje wejście od nowa własnym parserem. Gdy parser jest pobłażliwy, błąd, który powinien zostać zgłoszony, zamienia się w cichą zmianę wartości — a strona wciąż pokazuje uspokajający zielony komunikat „Sformatowano pomyślnie".

1. Zjawisko: jedno wejście, cztery ścieżki, cztery wyniki

Testowano na Python 3.11.1, Node v22.13.1 i jq 1.7. Dziesięć ręcznie zbudowanych krótkich łańcuchów JSON trafiło przez standardowe wejście, bez zapisu na dysk i bez zmiany konfiguracji. Ścieżki: json.loads, JSON.parse, jq -c ., python -m json.tool. ERR oznacza wyjątek i przerwanie, SILENT oznacza brak błędu, ale wynik różni się od wejścia.

Próbka 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

Cztery z dziesięciu próbek dały „bez błędu, ale inne dane": S4, S6, S7, S8. Kto ufa tylko zielonemu komunikatowi, z czterdziestoprocentowym prawdopodobieństwem trzyma już nie te dane, które wysłał. Podstępne jest to, że żaden z tych przypadków nie figuruje na liście błędów składniowych — plik da się otworzyć, skopiować, wyeksportować, błąd siedzi w treści, nie w składni.

2. Przyczyna: warstwa składniowa i semantyczna to dwie różne rzeczy

RFC 8259 mówi to wprost: sekcja 1.2 zaleca unikać praktyk dozwolonych przez ECMA-404 dla maksymalnej interoperacyjności; sekcja 4 wymaga unikalnych nazw obiektów jedynie jako SHOULD, nie określając postępowania z powielonymi kluczami; sekcja 6 przy liczbach daje tylko zalecenia interoperacyjności. ECMA-404 idzie dalej i wprost ogłasza się „agnostyczną wobec semantyki liczb" — reguluje tylko zapis ciągu cyfr, nie przechowywanie w języku. Specyfikacja zostawia więc implementacjom szeroki pas graniczny.

„Formatowanie" rozpada się tak na dwie warstwy: składniowa decyduje, czy tekst jest legalny, semantyczna decyduje, jaką wartość z niego powstanie. Pierwsza ma standardową odpowiedź, druga nie. To, że jq czyta 01 jako 1 i pomija początkowy BOM, to wybór warstwy semantycznej; że Node wciska 20-cyfrową liczbę całkowitą w double IEEE 754, to wybór środowiska uruchomieniowego; że wszystkie cztery ścieżki przy {"a":1,"a":2} zachowują ostatnią wartość, to wspólna konwencja dużych parserów. Nic nie narusza „parsowalności", ale wszystko zmienia dane.

Jak naprawić błędy parsowania JSON działa na warstwie składniowej i przekłada treść błędu na współrzędne. Ten artykuł zajmuje się krokiem wcześniejszym: jeśli narzędzie nie zgłosi żadnego błędu, nie dostaniesz nawet współrzędnych.

3. Cztery ciche zmiany danych, jedna po drugiej

Wyjęte osobno, cztery ciche próbki zmieniają się tak nieznacznie, że przy pobieżnym czytaniu ich nie zauważysz:

{"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}

Zero wiodące i BOM to typ „wejście już było niepoprawne, ale narzędzie było wyrozumiałe". 01 jest w standardowym JSON nielegalne (RFC 8259 sekcja 6), BOM nie powinien zaczynać tekstu JSON; Python i Node oba odrzucają, jq idzie dalej. Przy czyszczeniu logów to wygodne, przy kontroli kontraktu zgubne: upstream sądzi, że wysłał błędne dane do poprawy, podczas gdy downstream trzyma już „normalnie wyglądający" wynik.

Duża liczba całkowita to typ „wejście w pełni legalne, ale środowisko jej nie udźwignie". 9007199254740993 to legalna liczba JSON, leży jednak powyżej 2^53-1; JSON.parse potrafi tylko 9007199254740992. Python int i jq zachowują oryginał. Jeśli to numer zamówienia lub Snowflake ID, jedna zmieniona ostatnia cyfra oznacza, że rozliczenie się nie zgadza.

Powielone klucze to typ „specyfikacja milczy, implementacja wybiera". Wszystkie cztery ścieżki po cichu biorą ostatnią wartość, bez żadnego ostrzeżenia. RFC 8259 mówi tylko SHOULD, więc to nie błąd — ale przy łączeniu konfiguracji czy scalaniu patchy to najtrudniejsza do wyśledzenia klasa problemów.

4. Lokalizowanie błędu: kto daje wiersz i kolumnę, kto tylko jedno zdanie

Także przy sześciu nieudanych próbkach ilość informacji mocno się różni. JSONDecodeError Pythona daje jednocześnie msg, lineno, colno i pos; Node v22 zgłasza position N (line 1 column M) plus fragment źródła przy błędzie; python -m json.tool wykorzystuje informacje Pythona; jq daje tylko parse error: ... at line N bez przesunięcia znakowego.

W jednowierszowym zminifikowanym JSON wiersz i kolumna są bezużyteczne — wszystko jest w wierszu 1. Potrzebne jest przesunięcie znakowe. Poniższy skrypt sprowadza kontrolę do „błąd + offset + kontekst":

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())

Jest surowszy niż jq . (odrzuca zero wiodące i BOM) i daje e.pos. Jak zamienić line 1 column 23611 na przesunięcie bajtowe i dlaczego przesunięcie znakowe i bajtowe w dokumentach CJK różnią się trzykrotnie, opisano w Debugowanie zminifikowanego JSON — tu tego nie powtarzamy.

5. Naprawa: rozdziel formatowanie na „ścisłą kontrolę" + „upiększenie"

Wniosek jest prosty: jeden przycisk „Formatuj" nie powinien jednocześnie odpowiadać za kontrolę i upiększenie. Najpierw ścisłym parserem ustal legalność, potem wybierz narzędzie do układu. Pierwszy krok odetniesz jednym poleceniem:

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

Pierwsza linia odpowiada tylko na „da się sparsować?", druga sprawdza „czy struktura jest taka, jakiej oczekuję?". Przy dużych liczbach całkowitych czytaj Pythonem lub jq, nie JSON.parse; gdy źródło może mieć BOM, czytaj jawnie z encoding='utf-8-sig' zamiast liczyć na automatyczne pominięcie; gdy klucze mogą się powtarzać, przeskanuj wejście osobno przed parsowaniem, bo żadna z czterech ścieżek nie ostrzeże. Wpisane do CI to dokładnie pipeline z Edytory, wiersz poleceń i łańcuchy automatyzacji, tylko z dodaną warstwą asercji wierności danych.

6. Jak wybrać: tabela porównawcza czterech ścieżek

Ścieżka Mocna strona Ryzyko Dla
python json.loads / json.tool Ścisły, daje wiersz/kolumnę i przesunięcie znakowe, liczby całkowite bez utraty Odrzuca zero wiodące i BOM, kodowanie trzeba obsłużyć jawnie Kontrola kontraktów, CI, analiza logów
node JSON.parse Natywny w przeglądarce, błąd zawiera fragment źródła Duże liczby całkowite cicho ucina, BOM powoduje błąd Debugowanie frontendu, narzędzia w przeglądarce
jq -c . Przyjazny potokom, najszybszy wynik Zero wiodące i BOM przechodzą cicho, brak przesunięcia znakowego Potoki operacyjne, szybkie wyciąganie pól
Narzędzia przeglądarkowe /parse, /editor Widok drzewa, sortowanie kluczy, kopiowanie ścieżki, bez instalacji Zachowanie zależy od parsera strony, trzeba raz sprawdzić Ręczny przegląd, doraźne szukanie błędów

Kluczowe pytanie nie brzmi „kto ma więcej funkcji", lecz „czy to narzędzie kłamie o moich danych?". Dla numerów zamówień, kwot czy ID urządzeń wybierz Python lub narzędzie z wyraźnie zadeklarowaną wiernością danych; do czystego podglądu struktury bez zapisu wystarczy przeglądarka. /parse na tej stronie sprawdza i formatuje w przeglądarce — idealne, by „najpierw zobaczyć", ale przy dużych liczbach całkowitych warto przejść powyższym skryptem.

7. Prywatność to tylko jedna z granic

„Czy moje dane zostaną wysłane?" to najczęstsze pytanie do narzędzi online, ale jest niezależne od „czy moje dane zostaną zmienione?". Lokalne narzędzie offline i tak może uciąć 20-cyfrową liczbę całkowitą; z kolei narzędzie online ze ścisłymi błędami może wysłać oryginał POST-em na serwer. Obie właściwości trzeba sprawdzić osobno, jedna nie zastępuje drugiej. FAQ o prywatności daje trzy dwuminutowe kontrole; ten artykuł dodaje drugą połowę — z poniższą listą próbek potwierdź, że narzędzie nie zmienia wartości po cichu.

8. Kontrolna lista do skopiowania

Zapisz S1–S10 jako cases.txt, jedna próbka na wiersz, wklej do każdego kandydata i sprawdź trzy rzeczy: czy próbki z błędem faktycznie zawiodły? czy błąd podał konkretną pozycję? czy wynik udanej próbki jest znak po znaku zgodny z wejściem? Szczególnie sprawdź S4, S6, S7, S8 — wystarczy jeden wiersz SILENT, by dana ścieżka nie nadawała się samodzielnie do walidacji danych.

Dodaj trzy reguły inżynierskie: ścisłą kontrolę do hooka pre-commit, by nie odkryć rozjazdu liczb całkowitych dopiero na produkcji; pliki zapisuj między językami jako UTF-8 bez BOM; dla JSON od osób trzecich zachowaj surowe ciało odpowiedzi, a sformatowany wynik używaj tylko do czytania, nigdy do zapisu zwrotnego.

Często zadawane pytania

Czy udane formatowanie oznacza, że JSON jest w porządku?

Nie. S4, S6, S7 i S8 wszystkie „przeszły" formatowanie, ale zero wiodące, BOM, 20-cyfrowa liczba całkowita i powielony klucz zostały przepisane lub połknięte. Formatowanie dowodzi tylko, że jakiś parser przyjął tekst, nie że wartości odpowiadają wysłanym. Krytyczne pola sprawdzaj wartość po wartości.

Dlaczego jq przyjmuje zero wiodące i BOM, które Python odrzuca?

jq ma własny lekser, przy liczbach jak 01 jest pobłażliwy i pomija początkowy BOM; Python i Node trzymają się RFC 8259 ściśle. To nie błąd wersji, lecz różnica między parserem tolerancyjnym a ścisłym. Przy czyszczeniu pierwszy oszczędza pracy, przy kontroli kontraktu drugi jest obowiązkowy.

Jak zapobiec zmianie dużej liczby całkowitej na 9007199254740992?

Number w JavaScripcie to double IEEE 754 z bezpieczną górną granicą 2^53-1. Odczyt 20-cyfrowego ID przez JSON.parse nieuchronnie je utnie. Zapisuj takie ID jako łańcuch albo użyj Python int, jq lub parsera z BigInt. Jeśli frontend naprawdę potrzebuje liczby, zamień to pole na łańcuch jeszcze przed parsowaniem.

Narzędzie online czy lokalna linia poleceń — co wybrać?

Rozdziel według wrażliwości i celu: tylko podgląd struktury czy znalezienie pola → przeglądarka jest najszybsza; do potoku i z asercjami wierności danych → python -m json.tool plus ścisła kontrola. W obu przypadkach najpierw uruchom S1–S10, porównaj błędy i wyniki z oczekiwaniem, a dopiero potem zdecyduj, jaką klasę danych wpuścić do środka.