Битый JSON: JSONDecodeError и как его читать
Одинарные кавычки, висячая запятая, True вместо true — три классические причины JSONDecodeError, и текст ошибки, который честно называет строку и колонку.
Редакция Питоники
До сих пор мы кормили json.loads правильными строками — и он послушно возвращал словари. Но в реальности текст приходит из файлов, из сети, от коллег, и половина его написана руками. Кто-то сохранил словарь Python через str(), кто-то поставил запятую перед скобкой — и разбор падает с JSONDecodeError. Сегодня мы научимся вызывать эту ошибку специально, читать её текст и обкладывать разбор страховкой.
Главную мысль урока сформулируем сразу. Текст ошибки JSONDecodeError честно называет строку и колонку, где парсер споткнулся, — читать его выгоднее, чем гадать. Почти каждый битый JSON чинится за минуту, если знать три классические причины и уметь найти в сообщении колонку.
Откуда берётся битый текст? Список привычный: конфиги, которые правила человеческая рука; выгрузки из старых систем, где JSON писали конвертером из Excel; ответы чужих сервисов, у которых «почти JSON» — фича, а не баг; и, конечно, наши собственные str(словарь) в логах, которые кто-то спустя месяц попытался разобрать обратно. Битый JSON — нормальная часть жизни, а не ЧП.
Правильный JSON — как контрольный образец
Сначала зафиксируем эталон: двойные кавычки вокруг имён ключей и строковых значений, никаких запятых перед закрывающими скобками, логические значения с маленькой буквы. Такой текст json.loads читает без вопросов.
import json
good = '{"name": "Анна", "age": 25}'
data = json.loads(good)
print(data["name"], data["age"])
Анна 25
Заметь: сама строка обёрнута в одинарные кавычки Python, а внутри неё — двойные. Это не каприз: внешние кавычки — синтаксис Python, внутренние — синтаксис JSON. Перепутаешь — получишь ошибку ещё на этапе записи строки или, что коварнее, правильный с виду питоновский текст с битым JSON внутри.
Причина первая: одинарные кавычки
Самая частая причина падения: в строку положили запись словаря Python, а не JSON. Словарь печатается с одинарными кавычками, и на глаз тексты почти неотличимы — но парсер JSON одинарные кавычки не признаёт вовсе.
import json
broken = "{'name': 'Анна'}"
try:
json.loads(broken)
except json.JSONDecodeError as e:
print("Ошибка:", e)
print("Строка:", e.lineno, "колонка:", e.colno)
Ошибка: Expecting property name enclosed in double quotes: line 1 column 2 (char 1) Строка: 1 колонка: 2
Читаем сообщение по частям. Expecting property name enclosed in double quotes — «ждал имя свойства в двойных кавычках»: парсер дошёл до 'name' и не понял такой записи. line 1 column 2 (char 1) — место: первая строка, вторая колонка, то есть ровно первая одинарная кавычка. Ошибка не только сказала, что не так, но и ткнула пальцем, где.
| Запись | В словаре Python | В JSON |
|---|---|---|
| Кавычки | одинарные или двойные | только двойные |
| Логические значения | True / False | true / false |
| Отсутствие значения | None | null |
| Запятая перед ] или } | допустима | запрещена |
Ещё две классики: висячая запятая и True
Второе место битвы — запятая, поставленная по питоновской привычке перед закрывающей скобкой. В списках и словарях Python она разрешена и даже поощряется, в JSON — ошибка.
import json
broken = '{"a": 1, "b": 2,}'
try:
json.loads(broken)
except json.JSONDecodeError as e:
print("Ошибка:", e)
Ошибка: Illegal trailing comma before end of object: line 1 column 16 (char 15)
Здесь Python даже формулирует по-человечески: «недопустимая висячая запятая перед концом объекта» — и называет колонку 16, где стоит }. Третья классика — логическое значение с большой буквы, по-питоновски:
import json
broken = '{"active": True}'
try:
json.loads(broken)
except json.JSONDecodeError as e:
print("Ошибка:", e)
Ошибка: Expecting value: line 1 column 12 (char 11)
Expecting value — «ждал значение»: в JSON на этом месте бывают числа, строки, true, false, null — но не True с большой буквы. Колонка 12 указывает на заглавную T. Та же история с None вместо null: формат помнит своё javascript-происхождение и пишет константы с маленькой буквы.
Висячая запятая коварна и в списках верхнего уровня: "[1, 2,]" упадёт с той же Illegal trailing comma, только before end of array. Правило одно для всех скобок JSON: запятая стоит между элементами, а не перед закрывающей скобкой. Питоновская привычка ставить запятую после каждого элемента — для многострочных литералов — в JSON-текст не переносится никогда.
Читать ошибку: строка, колонка, символ
Пропущенная запятая между парами ключ-значение — случай посложнее: парсер долго идёт по правильному тексту и спотыкается не там, где допущена описка, а где она стала видна. Сравним колонку из ошибки с реальным местом поломки:
import json
broken = '{"name": "Анна", "city": "Казань" "age": 25}'
try:
json.loads(broken)
except json.JSONDecodeError as e:
print("Ошибка:", e.msg)
print("Строка:", e.lineno, "колонка:", e.colno)
print("Символ там:", repr(broken[e.colno - 1]))
Ошибка: Expecting ',' delimiter Строка: 1 колонка: 35 Символ там: '"'
Описка — пропущенная запятая после "Казань", но колонка 35 указывает на открывающую кавычку "age": именно там парсер понял, что строки склеились. У объекта ошибки есть атрибуты msg, lineno и colno — их можно использовать в коде, чтобы собрать внятное сообщение для лога, как мы только что сделали.
В короткой строке колонка находит поломку мгновенно. В файле на пятьсот строк атрибут lineno не менее ценен: строка 347, колонка 12 — и редактор открывается ровно в нужном месте, минуя увлекательный ручной поиск запятой. Именно поэтому в логах ошибок разбора печатают всё сообщение целиком: line и column там не для красоты.
try/except вокруг loads
Когда текст приходит извне, вопрос не «упадёт ли», а «что делать, когда упадёт». Ответ — try/except json.JSONDecodeError: пробуем разобрать, при неудаче возвращаем запасной план. Ловить нужно именно JSONDecodeError — он говорит «текст не JSON», а не «что-то сломалось вообще».
import json
def load_settings(text):
try:
return json.loads(text), None
except json.JSONDecodeError as e:
return None, f"битый JSON: {e}"
data, err = load_settings('{"theme": "dark"}')
print(data)
print(err)
data, err = load_settings("{'theme': 'dark'}")
print(data)
print(err)
{'theme': 'dark'}
None
None
битый JSON: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)Кстати, о точности ловли: JSONDecodeError — подкласс ValueError, поэтому except ValueError тоже сработает. Но ловить лучше конкретного наследника: имя в except — это документация («здесь ждём плохой текст»), а не экономия строк. Голый except: — худший вариант: он проглотит и опечатку в имени переменной, и прерывание с клавиатуры, и настоящую ошибку логики, замаскировав её под «битый JSON». На ревью такой код отправляют на переписывание сразу.
Функция возвращает пару «данные или ошибка» — вызывающий код сам решает, что делать: подставить настройки по умолчанию, показать пользователю сообщение, записать в лог. Это общий шаблон для любого внешнего текста: файл, ответ сервера, строка из базы.
import json
data = {"city": "Москва", "temp": 21}
text = str(data)
try:
json.loads(text)
except json.JSONDecodeError:
print("str() годится только для печати, не для обмена")
safe = json.dumps(data, ensure_ascii=False)
print(json.loads(safe))
str() годится только для печати, не для обмена
{'city': 'Москва', 'temp': 21}json.dumps — обратная функция: она превращает словарь в настоящую JSON-строку, с двойными кавычками и true вместо True. Круг dumps — затем loads возвращает те же данные. Если ты сам создаёшь текст JSON в Python — всегда через dumps, никогда руками и никогда через str.
У dumps есть два параметра, которые стоит запомнить сразу: ensure_ascii=False — оставить кириллицу буквами, а не процентными кодами, и indent=2 — отступы и переводы строк, чтобы файл читал человек, а не только парсер. Для конфигов это комбинация по умолчанию: json.dumps(data, ensure_ascii=False, indent=2).
Чиним руками и защищаемся навсегда
Маленький битый файл чаще всего чинится руками. Порядок осмотра, отработанный на трёх классиках выше, умещается в короткий чек-лист:
- все кавычки вокруг ключей и строковых значений — двойные;
- константы — true, false, null с маленькой буквы, без питоновских True и None;
- перед каждой закрывающей скобкой нет запятой;
- между парами ключ-значение запятые стоят, а не пропущены;
- файл не пустой и начинается со скобки, а не с текста ошибки;
- если чек-лист не помог — ошибка называет строку и колонку следующего подозреваемого.
Проверяем:
import json
fixed = '{"name": "Пётр", "age": 30}'
print(json.loads(fixed)["name"])
print(json.loads(fixed)["age"])
Пётр 30
А чтобы вопрос «а вдруг там мусор» больше никогда не звучал, оформим разбор в маленькую функцию с умолчанием — она пригодится нам в уроках про API-ответы, начиная с разбора ответа:
import json
def safe_loads(text, default=None):
try:
return json.loads(text)
except json.JSONDecodeError:
return default
print(safe_loads('{"ok": true}'))
print(safe_loads("{ok: true}", default="передан не JSON"))
print(safe_loads("", default={}))
{'ok': True}
передан не JSON
{}Третий вызов — задел на будущее: пустая строка тоже не JSON, и функция честно вернула пустой словарь. В живом проекте, где текст приходит по сети, такая обёртка экономит нервы: сервер моргнул, отдал пустоту — программа пережила.
Для проверки файла на своём компьютере есть и готовый инструмент без единой строки кода: команда python -m json.tool файл.json. Разберёт — напечатает файл с отступами; споткнётся — покажет ту же строку и колонку, что и в наших примерах. Удобно для конфигов, которые правились руками: одна команда вместо сеанса гадания.
Битый текст — не всегда вина коллег: иногда сервер по ошибке отдаёт HTML вместо JSON. Вот как это выглядит в живом коде — фрагмент для своего компьютера, в песочнице сети нет:
# На своём компьютере, после pip install requests:
import json
import requests
r = requests.get("https://example.com/api/v1/status")
try:
data = r.json() # то же, что json.loads(r.text)
except json.JSONDecodeError:
print("Сервер прислал не JSON:", r.text[:60])
Ошибка разбора — это не катастрофа, а сигнал: текст не соответствует формату, и вот точное место. Путь к живым ответам продолжается в уроке про URL, а если пропустил — вернись к путям к значениям из урока 8: без них битый JSON не починишь, ведь после починки значения всё равно доставать цепочками.
Битый JSON — не катастрофа, а координаты: ошибка называет причину и колонку, где формат нарушен.
Сначала предскажи ответ в голове — это главный навык программиста.
import json
try:
json.loads('{"x": 1,}')
except json.JSONDecodeError as e:
print(e.msg)
import json
def try_load(text):
try:
return json.loads(text)
except json.JSONDecodeError:
return "fail"
print(try_load('{"a": [1, 2]}'))
print(try_load("{'a': 1}"))
import json
text = '{"done": true, "count": 3}'
data = json.loads(text)
print(data["done"], data["count"])
1. Какие кавычки допустимы в JSON?
2. Что означает column 2 в тексте JSONDecodeError?
3. Почему строка '{"active": True}' не разбирается?
4. Как безопасно разобрать текст, который может оказаться битым?
5. Что будет, если передать str({"a": 1}) в json.loads?
Напиши функцию check(text): она пытается разобрать текст через json.loads и возвращает строку "ok: " плюс значение поля name, а при JSONDecodeError — строку "битый JSON". Прогони её на трёх строках: правильной, с одинарными кавычками и правильной с двумя полями.
Почему json.loads не читает словарь с одинарными кавычками?
Потому что это не JSON. Стандарт формата разрешает только двойные кавычки; запись с одинарными — синтаксис словаря Python. Ошибка выглядит как Expecting property name enclosed in double quotes. Для создания JSON-текста из словаря используй json.dumps, а не str().
Как узнать место ошибки в JSONDecodeError?
Прочитать текст исключения: Expecting value: line 1 column 12 (char 11) — причина, строка и колонка. В коде доступны атрибуты e.msg, e.lineno и e.colno; по colno можно даже показать пользователю проблемный символ: text[e.colno - 1].
Как обработать битый JSON в Python?
Обернуть разбор в try/except json.JSONDecodeError и в ветке except вернуть умолчание или сообщение. Удобно оформить обёрткой вида safe_loads(text, default=None) — она заменяет собой десяток проверок и не даёт программе падать на внешних данных.
Чем запись словаря Python отличается от JSON?
Кавычками (в Python допустимы одинарные), константами (True, False, None против true, false, null) и висячими запятыми (в Python можно, в JSON нельзя). На глаз тексты похожи, но json.loads принимает только строгий JSON-вариант.
Понравился урок? Сошлитесь на него
«Текст ошибки JSONDecodeError честно называет строку и колонку, где парсер споткнулся, — читать его выгоднее, чем гадать.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
json · Урок 7
Вложенные структуры: списки в словарях и наоборот
Настоящие данные не плоские: список товаров внутри заказа, массив объектов в ответе API. Учимся читать вложенный JSON сверху вниз и не теряться в глубине.
json · Урок 8
Путь к значению: достаём данные из глубины
Цепочки data["user"]["name"] и data["items"][0]["price"], KeyError у отсутствующих полей и get с умолчанием — спуск по типичному ответу API без падений.
json · Урок 14
json.loads на ответе API: от текста к данным
Текст, json.loads, словарь, поля: собираем полный конвейер разбора ответа API — с проверкой структуры, обработкой пустого и битого тела.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
BeautifulSoup / Scrapy · Урок 8
Этичный парсинг: robots.txt, задержки и чтобы не забанили
Парсер собрал три тысячи страниц — а утром каждый запрос возвращает 403. Читаем robots.txt, представляемся по имени, держим вежливый темп и отвечаем на 429 так, чтобы дверь не закрылась навсегда.
ошибка 403 при парсинге что делатьошибка 429 python
pytest · Урок 3
Отчёт pytest: подробный вывод -v и разбор падения
Зелёная точка — скучный отчёт, и это хорошо. Настоящая сила pytest раскрывается при падении: он показывает строку, ожидание, реальность и разницу между ними.
pytest читать отчётpytest ожидание реальность
json · Урок 2
json.loads: чтение JSON из строки
Обратная дорога: текст JSON становится словарём Python одной командой json.loads — и по ключам можно ходить, считать и менять значения.
json loads pythonjson.loads