Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 9 из 20 Начальный 30 мин 110 XP

Битый JSON: JSONDecodeError и как его читать

Одинарные кавычки, висячая запятая, True вместо true — три классические причины JSONDecodeError, и текст ошибки, который честно называет строку и колонку.

Редакция Питоники

До сих пор мы кормили json.loads правильными строками — и он послушно возвращал словари. Но в реальности текст приходит из файлов, из сети, от коллег, и половина его написана руками. Кто-то сохранил словарь Python через str(), кто-то поставил запятую перед скобкой — и разбор падает с JSONDecodeError. Сегодня мы научимся вызывать эту ошибку специально, читать её текст и обкладывать разбор страховкой.

Главную мысль урока сформулируем сразу. Текст ошибки JSONDecodeError честно называет строку и колонку, где парсер споткнулся, — читать его выгоднее, чем гадать. Почти каждый битый JSON чинится за минуту, если знать три классические причины и уметь найти в сообщении колонку.

Откуда берётся битый текст? Список привычный: конфиги, которые правила человеческая рука; выгрузки из старых систем, где JSON писали конвертером из Excel; ответы чужих сервисов, у которых «почти JSON» — фича, а не баг; и, конечно, наши собственные str(словарь) в логах, которые кто-то спустя месяц попытался разобрать обратно. Битый JSON — нормальная часть жизни, а не ЧП.

Правильный JSON — как контрольный образец

Сначала зафиксируем эталон: двойные кавычки вокруг имён ключей и строковых значений, никаких запятых перед закрывающими скобками, логические значения с маленькой буквы. Такой текст json.loads читает без вопросов.

правильный JSON разбирается молча
import json

good = '{"name": "Анна", "age": 25}'
data = json.loads(good)
print(data["name"], data["age"])
Вывод
Анна 25

Заметь: сама строка обёрнута в одинарные кавычки Python, а внутри неё — двойные. Это не каприз: внешние кавычки — синтаксис Python, внутренние — синтаксис JSON. Перепутаешь — получишь ошибку ещё на этапе записи строки или, что коварнее, правильный с виду питоновский текст с битым JSON внутри.

Причина первая: одинарные кавычки

Самая частая причина падения: в строку положили запись словаря Python, а не JSON. Словарь печатается с одинарными кавычками, и на глаз тексты почти неотличимы — но парсер 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 / Falsetrue / false
Отсутствие значенияNonenull
Запятая перед ] или }допустимазапрещена

Ещё две классики: висячая запятая и True

Второе место битвы — запятая, поставленная по питоновской привычке перед закрывающей скобкой. В списках и словарях Python она разрешена и даже поощряется, в JSON — ошибка.

trailing comma
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, где стоит }. Третья классика — логическое значение с большой буквы, по-питоновски:

True вместо true
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». На ревью такой код отправляют на переписывание сразу.

Функция возвращает пару «данные или ошибка» — вызывающий код сам решает, что делать: подставить настройки по умолчанию, показать пользователю сообщение, записать в лог. Это общий шаблон для любого внешнего текста: файл, ответ сервера, строка из базы.

str против json.dumps
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-ответы, начиная с разбора ответа:

safe_loads - разбор без сюрпризов
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. Вот как это выглядит в живом коде — фрагмент для своего компьютера, в песочнице сети нет:

сервер прислал не 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])
Блок для запуска на своём компьютере: в браузерной песочнице сети нет. Сама проверка — try/except json.JSONDecodeError — ровно та, что мы отработали выше.

Ошибка разбора — это не катастрофа, а сигнал: текст не соответствует формату, и вот точное место. Путь к живым ответам продолжается в уроке про 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"])
Проверь себя
0 / 5

1. Какие кавычки допустимы в JSON?

2. Что означает column 2 в тексте JSONDecodeError?

3. Почему строка '{"active": True}' не разбирается?

4. Как безопасно разобрать текст, который может оказаться битым?

5. Что будет, если передать str({"a": 1}) в json.loads?

Карточки терминов
Запомнено: 0 / 6
Практика

Напиши функцию check(text): она пытается разобрать текст через json.loads и возвращает строку "ok: " плюс значение поля name, а при JSONDecodeError — строку "битый JSON". Прогони её на трёх строках: правильной, с одинарными кавычками и правильной с двумя полями.

practice.py
Вопросы и ответы по уроку

Почему 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.