JSON Toolkit

Той самий JSON — чотири інструменти дають чотири результати: як обрати форматувальник

Головне: Форматування не є безвтратним. Один і той самий JSON із нулем на початку, BOM або 20-значним цілим по-різному обробляють Python, Node, jq і json.tool: одні повідомляють про помилку, інші мовчки змінюють дані. Десять власних тестів чотирьох шляхів дають таблицю вибору та готовий сценарій строгої перевірки.

Intermediate~6 хв читанняОпубліковано 2026-11-03Оновлено 2026-11-03
Чотири шляхи розбору повертають різні результати для того самого JSON

Висновок: найважливіша відмінність між JSON-форматувальниками — не кількість пробілів відступу, а те, що той самий вхід десь викликає помилку, а десь мовчки переписує дані. Один зразок з граничними значеннями дає більше, ніж будь-який список функцій.

Форматування часто вважають «просто перестановкою тексту». Насправді кожен шлях заново інтерпретує його власним парсером. Якщо парсер поблажливий, помилка, яка мала б з'явитися, перетворюється на тиху зміну значення, а сторінка й далі показує заспокійливе зелене «Успішно відформатовано».

1. Факт: один вхід, чотири шляхи, чотири результати

Тест на Python 3.11.1, Node v22.13.1 і jq 1.7. Десять коротких JSON-рядків, зібраних вручну, подавалися через стандартний ввід без запису на диск і без зміни налаштувань. Шляхи: json.loads, JSON.parse, jq -c ., python -m json.tool. ERR — виняток і зупинка; SILENT — помилки немає, але вихід відрізняється від входу.

Зразок 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 точно SILENT 9007199254740992 OK точно 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

Чотири з десяти зразків дали «без помилки, але зі зміненими даними»: S4, S6, S7, S8. Якщо вірити лише зеленому індикатору, є сорок відсотків шансу тримати не ті самі дані. Найгірше, що жоден із цих випадків не потрапляє до списку синтаксичних помилок: файл відкривається, копіюється, експортується. Вада у вмісті, а не в синтаксисі.

2. Причина: синтаксис і семантика — різні шари

RFC 8259 говорить прямо: розділ 1.2 радить уникати задля сумісності практик, які дозволяє ECMA-404; розділ 4 вимагає унікальності імен лише як SHOULD, не визначаючи поводження з дубльованими ключами; розділ 6 дає щодо чисел лише рекомендації сумісності. ECMA-404 іде далі й оголошує себе «незалежною від семантики чисел»: регламентується подання, а не зберігання. Тобто специфікація свідомо лишає широку межову зону реалізаторам.

Тому «форматування» розпадається на два шари: синтаксичний вирішує, чи текст легітимний, семантичний — яким значенням він стане. Перший має стандартну відповідь, другий — ні. jq, читаючи 01 як 1 і пропускаючи початковий BOM, робить семантичний вибір; Node, втискаючи 20-значне ціле у double IEEE 754, — вибір середовища виконання; усі чотири шляхи, лишаючи останнє значення {"a":1,"a":2}, — спільна домовленість парсерів. Ніщо не порушує «можна розібрати», але все змінює дані.

Як виправити помилки розбору JSON працює на синтаксичному шарі й перекладає текст помилки в координати. Ця стаття — про крок перед ним: якщо інструмент узагалі не дав помилки, координат не отримаєш.

3. Чотири тихі переписування окремо

Винесені окремо, чотири зразки показують зміни, надто малі, щоб їх помітити на око:

{"a": 01}                          // нуль на початку: jq видає {"a":1}
\ufeff{"a": 1}                     // BOM: jq пропускає перші три байти
{"id": 9007199254740993}           // 20-значне ціле: JSON.parse видає ...992
{"a": 1, "a": 2}                   // дубль ключа: усі чотири лишають {"a":2}

Нуль на початку й BOM — тип «вхід уже був некоректним, але інструмент поблажливий». 01 незаконний у стандартному JSON (RFC 8259 §6), BOM не повинен починати JSON-текст; Python і Node відхиляють, jq продовжує. Зручно під час очищення логів, згубно під час перевірки контракту: відправник вважає, що надіслав хибні дані, які треба виправити, а отримувач уже має «нормальний на вигляд» результат.

Велике ціле — тип «вхід цілком легітимний, але середовище не вміщає». 9007199254740993 — цілком легітимне JSON-число, та воно перевищує 2^53-1, і JSON.parse може подати лише 9007199254740992. int Python і jq зберігають значення. Якщо це номер замовлення чи Snowflake ID, змінена остання цифра означає розбіжність у звірці.

Дубльовані ключі — тип «специфікація не каже, кожна реалізація обирає». Усі чотири шляхи мовчки беруть останнє значення без попередження. RFC 8259 лише каже SHOULD, тож це не баг, але це найважчий для пошуку клас проблем при злитті конфігів чи патчів.

4. Звіти про помилки: хто дає позицію, хто один рядок

Шість невдалих зразків теж різняться обсягом інформації. JSONDecodeError Python дає msg, lineno, colno і pos; Node v22 повідомляє position N (line 1 column M) із фрагментом проблемного тексту; python -m json.tool використовує дані Python; jq дає лише parse error: ... at line N без зміщення символу.

У мініфікованому одному рядку рядок і стовпець майже марні — усе на рядку 1. Потрібне зміщення символу. Скрипт нижче зводить перевірку до «помилка + offset + контекст»:

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

Він суворіший за jq . (відхиляє нуль на початку й BOM) і дає e.pos. Як перевести line 1 column 23611 у байтове зміщення та чому символьне й байтове зміщення відрізняються втричі в CJK-документах, описано в Налагодження мініфікованого JSON; тут не повторюємо.

5. Виправлення: розділити «сувору перевірку» й «прикрашання»

Висновок простий: одна кнопка «Форматувати» не має поєднувати перевірку й прикрашання. Спочатку суворий парсер визначає легітимність, потім обираємо подання. Перший крок виділяється одним рядком:

python3 -m json.tool input.json > /dev/null && echo "синтаксис OK" || echo "синтаксис хибний"
python3 -c "import json,sys; d=json.load(open(sys.argv[1],encoding='utf-8')); print(type(d).__name__, len(d))" input.json

Перший рядок відповідає лише «чи можна розібрати?», другий перевіряє «чи структура очікувана?». За наявності великих цілих читайте Python або jq замість JSON.parse; за можливого BOM явно вказуйте encoding='utf-8-sig' замість надії на автоматичний пропуск; за можливих дублів ключів скануйте окремо до розбору, бо жоден шлях не попередить. У CI це точно конвеєр із Редактори, командний рядок та інструментарій автоматизації плюс один шар тверджень про збереження даних.

6. Вибір: таблиця чотирьох шляхів

Шлях Сила Ризик Доцільно для
python json.loads / json.tool Суворий, дає рядок/стовпець і зміщення, цілі без втрати точності Відхиляє нуль на початку й BOM, кодування треба вказати Перевірка контракту, CI, розбір логів
node JSON.parse Нативний у браузері, помилка з фрагментом джерела Великі цілі тихо обрізаються, помилка на BOM Налагодження фронтенду, браузерні інструменти
jq -c . Зручний для конвеєрів, найшвидший результат Нуль на початку й BOM проходять тихо, без зміщення Операційні конвеєри, швидке витягування полів
Браузерні інструменти /parse, /editor Дерево, сортування ключів, копіювання шляху, без встановлення Поведінка залежить від парсера сторінки, перевірте один раз Ручний огляд, разове усунення несправностей

Ключ не «в кого більше функцій», а «чи збреше він про мої дані?». Для номерів замовлень, сум чи ID пристроїв обирайте Python або інструмент із явно заявленою точністю; для чистого огляду структури без збереження досить браузера. /parse цього сайту перевіряє й форматує в браузері — добре для «спершу подивитися», але великі цілі перевірте скриптом вище.

7. Приватність — лише одна з меж

«Чи завантажаться мої дані?» — найчастіше запитання до онлайн-інструментів, але воно не залежить від «чи зміняться мої дані?». Локальний офлайн-інструмент однаково може обрізати 20-значне ціле; навпаки, онлайн-інструмент із суворими помилками може надіслати оригінал POST-ом на сервер. Перевіряйте дві властивості окремо, одна не замінює іншу. FAQ про приватність дає три перевірки на дві хвилини; ця стаття додає другу половину: за списком зразків нижче переконайтеся, що інструмент не змінює значень мовчки.

8. Список самоперевірки, який можна скопіювати

Збережіть S1–S10 у cases.txt, по одному рядку на зразок, вставте в кожен кандидат і перевірте три речі: чи справді падають хибні зразки, чи дає падіння конкретну позицію, чи вихід успішного зразка збігається з входом досимвольно. Особливо S4, S6, S7, S8: один рядок SILENT означає, що цей шлях не можна використовувати самотужки для перевірки даних.

Додайте три інженерні обмеження: сувору перевірку — у pre-commit хук, а не виявлення розбіжності цілих у продакшені; файли писати як UTF-8 без BOM у всіх мовах; для JSON від третіх сторін зберігати сире тіло відповіді, а відформатований результат лише читати, ніколи не записувати назад.

Часті запитання

Чи означає успішне форматування, що JSON у порядку?

Ні. S4, S6, S7 і S8 усі «пройшли» форматування, але нуль на початку, BOM, 20-значне ціле й дубль ключа переписано або проковтнуто. Форматування доводить лише, що якийсь парсер прийняв текст, а не що значення дорівнюють надісланим. Перевіряйте критичні поля за кожним значенням.

Чому jq приймає нуль на початку й BOM, які Python відхиляє?

jq має власний лексичний аналізатор: він поблажливий до чисел на кшталт 01 і пропускає початковий BOM, тоді як Python і Node суворо дотримуються RFC 8259. Це не баг версії, а компроміс між поблажливим і суворим парсером. Перший економить час під час очищення, другий обов'язковий під час перевірки контракту.

Як не дати великому цілому стати 9007199254740992?

Number у JavaScript — це double IEEE 754 із безпечною межею 2^53-1. Читання 20-значного ID через JSON.parse неминуче обрізає його. Зберігайте такі ID рядками або використовуйте int Python, jq чи парсер із BigInt. Якщо фронтенду справді потрібне число, замініть це поле рядком до розбору.

Онлайн-інструмент чи локальний CLI — що обрати?

За чутливістю й метою: подивитися структуру чи знайти поле — браузер найшвидший; для конвеєра й тверджень точності — python -m json.tool плюс суворий чекер. В обох випадках спершу запустіть S1–S10, звірте помилки й виходи, а тоді вирішуйте, який клас даних подавати.