JSON (JavaScript Object Notation) — это текстовый формат записи структурированных данных: объекты из пар «ключ: значение», массивы, строки, числа и литералы true, false, null. Синтаксис взят из литералов JavaScript, но сам формат от языка не зависит: его читают и пишут Python, Java, C#, PHP, Go и почти любые другие языки. Сейчас он описан двумя согласованными стандартами — RFC 8259 и ECMA-404.
Содержание
Чтобы не путать, сразу разведу три понятия, которые в статьях о JSON часто склеивают:
- JSON-текст — сама строка вида
{"points": 1250}; - объект в памяти программы — словарь Python или объект JavaScript, который получается после разбора этой строки;
- Content-Type: application/json — HTTP-заголовок, которым отправитель сообщает получателю, что тело сообщения нужно читать как JSON.
Дальше по порядку: синтаксис, разбор в Python и JavaScript, заголовки Content-Type и Accept на рабочем сервере и частые ошибки.
Синтаксис JSON: из чего состоит документ
Пример — ответ сервера бонусной программы магазина по карте покупателя:
{
"name": "Анна",
"points": 1250,
"vip": false,
"city": null,
"last_orders": [101, 205, 318],
"card": {"number": "7001-2233", "level": "silver"}
}
Правила, по которым строится любой JSON-документ:
- объект заключен в фигурные скобки, пары разделяются запятыми, ключ отделяется от значения двоеточием;
- ключ — всегда строка в двойных кавычках;
- массив — упорядоченный список значений в квадратных скобках, значения могут быть разных типов;
- после последнего элемента запятая не ставится;
- комментариев в JSON нет;
- пробелы и переносы строк между элементами допустимы и ни на что не влияют.
Значение может быть одного из шести типов:
| Тип JSON | Пример | Python после json.loads |
JavaScript после JSON.parse |
|---|---|---|---|
| объект | {"a": 1} |
dict |
Object |
| массив | [1, "два"] |
list |
Array |
| строка | "Анна" |
str |
string |
| число | 1250, -3.5, 1e3 |
int или float |
number |
| логический | true, false |
True, False |
true, false |
| пусто | null |
None |
null |
Ограничения, о которые спотыкаются чаще всего. Строки только в двойных кавычках, спецсимволы экранируются обратной косой чертой (\", \n, \u0410). Числа только десятичные: без ведущих нулей (007 недопустимо), без шестнадцатеричной записи, без NaN и Infinity. Типа «дата» нет — даты передают строкой, обычно в формате ISO 8601 ("2026-09-23T10:00:00Z").
Повторяющиеся ключи в одном объекте стандарт не запрещает, но поведение в этом случае не определено — разные библиотеки поступают по-разному. Отправлять такие документы не стоит.
Разбор и сериализация: JSON.parse и json.loads
Перевод JSON-текста в объекты программы называют разбором (parsing, десериализация), обратный путь — сериализацией. Минимальный полный пример на Python (проверено на Python 3.14):
import json
raw = '{"name": "Анна", "points": 1250, "vip": false, "city": null, "last": [101, 205]}'
data = json.loads(raw) # JSON-текст -> объекты Python
print(type(data).__name__, data["points"] + 50, data["vip"], data["city"])
data["points"] = 1300
text = json.dumps(data, ensure_ascii=False) # объекты Python -> JSON-текст
print(text)
print(json.dumps({"name": "Анна"})) # по умолчанию не-ASCII экранируется
Вывод:
dict 1300 False None
{"name": "Анна", "points": 1300, "vip": false, "city": null, "last": [101, 205]}
{"name": "\u0410\u043d\u043d\u0430"}
Разбор по строкам. json.loads вернул обычный словарь, поэтому с points можно сразу считать. false и null превратились в False и None. При обратном переводе json.dumps без параметров экранирует кириллицу в \uXXXX — это валидный JSON, но читать его неудобно; ensure_ascii=False оставляет символы как есть. Для файлов есть пара json.load / json.dump, они работают с открытым файлом, а не со строкой.
То же самое в JavaScript (проверено в Node.js 24):
const raw = '{"name": "Анна", "points": 1250, "vip": false, "city": null}';
const user = JSON.parse(raw); // JSON-текст -> объект JS
console.log(user.points + 50, user.vip, user.city);
user.points = 1300;
console.log(JSON.stringify(user)); // объект JS -> JSON-текст
1300 false null
{"name":"Анна","points":1300,"vip":false,"city":null}
Никогда не разбирайте JSON через eval() или аналоги: если строка пришла извне, это выполнение чужого кода. JSON.parse и json.loads только читают данные.
Content-Type: application/json в HTTP
Content-Type — заголовок HTTP-сообщения, который указывает медиатип (MIME-тип) тела. Медиатип состоит из типа и подтипа: text/html, image/png, application/json. Для JSON зарегистрирован именно application/json; text/plain и application/javascript для него неверны.
Важно направление заголовка:
| Заголовок | Кто ставит | Что сообщает |
|---|---|---|
Content-Type в запросе |
клиент | «тело моего запроса — JSON» |
Content-Type в ответе |
сервер | «тело ответа — JSON» |
Accept в запросе |
клиент | «я умею читать такие форматы ответа» |
Если у GET-запроса нет тела, то и Content-Type в нем не нужен. Accept: application/json — это просьба, а не гарантия: сервер может ответить другим форматом или ошибкой 406 Not Acceptable, поэтому клиенту стоит проверять тип ответа.
Кодировка: по RFC 8259 JSON, передаваемый между системами, должен быть в UTF-8. Параметр charset для application/json не определен; запись application/json; charset=utf-8 встречается часто и на разбор не влияет, поэтому сервер должен ее принимать.
Рабочий пример: сервер, который проверяет тип контента
Учебный сервер на стандартной библиотеке Python: отдает JSON на GET и принимает JSON на POST. Проверки в POST идут в порядке «тип -> размер -> синтаксис -> смысл», и на каждую есть свой код ответа.
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
MAX_BODY = 64 * 1024 # лимит тела запроса, байт
class Handler(BaseHTTPRequestHandler):
def send_json(self, status, payload):
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
self.send_json(200, {"name": "Анна", "points": 1250})
def do_POST(self):
ctype = self.headers.get("Content-Type", "")
if ctype.split(";")[0].strip().lower() != "application/json":
return self.send_json(415, {"error": "ожидается application/json"})
length = self.headers.get("Content-Length", "")
if not length.isdigit():
return self.send_json(411, {"error": "нужен Content-Length"})
length = int(length)
if length > MAX_BODY:
return self.send_json(413, {"error": "слишком большое тело"})
try:
data = json.loads(self.rfile.read(length))
except (json.JSONDecodeError, UnicodeDecodeError, RecursionError):
return self.send_json(400, {"error": "некорректный JSON"})
points = data.get("points") if isinstance(data, dict) else None
if type(points) is not int: # bool - подкласс int, его отсекаем
return self.send_json(422, {"error": "нужно поле points (целое)"})
self.send_json(200, {"ok": True, "points": points})
HTTPServer(("127.0.0.1", 8000), Handler).serve_forever()
Запускаем python3 server.py и во втором терминале шлем запросы curl. Флаг -d без явного заголовка отправляет тело как application/x-www-form-urlencoded, поэтому третий запрос сервер отклоняет:
curl -i http://127.0.0.1:8000/
curl -X POST http://127.0.0.1:8000/ -H "Content-Type: application/json" -d '{"points": 1300}'
curl -X POST http://127.0.0.1:8000/ -d '{"points": 1300}'
curl -X POST http://127.0.0.1:8000/ -H "Content-Type: application/json" -d "{'points': 1300}"
curl -X POST http://127.0.0.1:8000/ -H "Content-Type: application/json" -d '{"points": "1300"}'
Фактический результат (код ответа дописан в скобках, строка Date опущена):
HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.14.7
Content-Type: application/json
X-Content-Type-Options: nosniff
Content-Length: 36
{"name": "Анна", "points": 1250}
{"ok": true, "points": 1300} [200]
{"error": "ожидается application/json"} [415]
{"error": "некорректный JSON"} [400]
{"error": "нужно поле points (целое)"} [422]
Что здесь важно. 415 Unsupported Media Type — ответ на неверный Content-Type, 400 — на синтаксически битый JSON, 422 — на правильный JSON с неподходящими данными. Разделять эти случаи полезно: клиент сразу видит, что чинить. RecursionError в списке исключений не лишний: тело из десятков тысяч открывающих скобок [[[[... укладывается в лимит 64 КБ, но разбор падает на глубине вложенности, и без этой строки обработчик рвет соединение без ответа (проверено на Python 3.14). Заголовок X-Content-Type-Options: nosniff запрещает браузеру «угадывать» тип по содержимому и толковать ответ иначе, чем объявлено.
Граница примера: это учебный сервер. http.server не предназначен для production — там нужны фреймворк или сервер приложений, аутентификация, HTTPS, журналирование и проверка схемы данных (например, по JSON Schema), а не одного поля.
Клиент: отправить JSON и проверить ответ
const res = await fetch("http://127.0.0.1:8000/", {
method: "POST",
headers: { "Content-Type": "application/json", "Accept": "application/json" },
body: JSON.stringify({ points: 1300 }),
});
const type = res.headers.get("content-type") ?? "";
if (!res.ok || !type.startsWith("application/json")) {
throw new Error(`Ожидали JSON, получили ${res.status} ${type}`);
}
const data = await res.json(); // разбор тела ответа как JSON
console.log(res.status, type, data);
200 application/json { ok: true, points: 1300 }
Проверка типа добавлена не случайно: res.json() пытается разобрать тело независимо от заголовка. Если прокси или сервер вернул HTML-страницу ошибки, без проверки читатель получит невнятный SyntaxError вместо понятного сообщения о статусе.
Частые ошибки при работе с JSON
Одиночные кавычки — самая частая ошибка у тех, кто пишет JSON руками или склеивает строку из словаря Python через str():
import json
bad = "{'name': 'Анна', 'points': 1250,}"
try:
json.loads(bad)
except json.JSONDecodeError as e:
print("Ошибка:", e)
good = '{"name": "Анна", "points": 1250}'
print(json.loads(good))
Ошибка: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
{'name': 'Анна', 'points': 1250}
Исправление — двойные кавычки и никакой запятой в конце. Строку JSON правильнее получать через json.dumps, а не собирать вручную. Лишняя запятая при верных кавычках в Python 3.14 дает отдельное сообщение Illegal trailing comma before end of object; в старых версиях текст ошибки другой. JavaScript на одиночные кавычки отвечает так (Node.js 24): SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2). Текст ошибки зависит от движка, поэтому в коде ловят тип исключения, а не сообщение.
Остальные типовые ловушки удобнее свести в таблицу. Все результаты получены прогоном на Python 3.14 и Node.js 24:
| Ситуация | Что происходит | Как делать |
|---|---|---|
Большое целое в JS: JSON.parse('{"id": 9007199254740993}') |
получается 9007199254740992 — точность потеряна, число больше 2^53 |
крупные идентификаторы передавать строкой |
json.dumps(float("nan")) в Python |
выводит NaN — это не валидный JSON |
json.dumps(..., allow_nan=False) бросит ValueError |
JSON.stringify({f: undefined, n: NaN}) |
поле f пропадает, NaN становится null |
не полагаться на такие значения в API |
Дата в Python: json.dumps({"t": date(...)}) |
TypeError: Object of type date is not JSON serializable |
переводить в строку ISO 8601 (d.isoformat()) |
Повторный ключ {"id": 1, "id": 2} |
Python оставляет последнее значение: {'id': 2} |
не отправлять дубли, валидировать вход |
Выводы
- JSON — текстовый формат из шести типов значений; ключи и строки только в двойных кавычках, без комментариев, лишних запятых,
NaNи дат как отдельного типа. - JSON-текст, объект в памяти и заголовок
Content-Type: application/json— три разные вещи: строку разбираютJSON.parseилиjson.loads, заголовок лишь сообщает, как читать тело. - В запросе с JSON-телом клиент ставит
Content-Type: application/json, желаемый формат ответа указывает вAccept; сервер помечает свой ответ тем жеContent-Type. - Сервер, принимающий JSON, проверяет тип, размер, синтаксис и смысл данных и отвечает разными кодами: 415, 413, 400, 422.
- Самые дорогие ошибки — потеря точности больших чисел в JavaScript и разбор недоверенных данных через
eval.
Где применяется / связь с практикой
Освойте тему на практике
JSON — основной формат большинства современных веб-API: мобильные приложения, фронтенд на React или Vue, микросервисы и интеграции с внешними системами обмениваются именно им. Как только API становится больше двух-трех методов, вопросы из этой статьи превращаются в проектные решения: какие коды ответа возвращать, как описать схему данных, как версионировать контракт и документировать его в OpenAPI. Этим занимается курс «Проектирование API».
Освойте тему на практике
Если хочется сначала посмотреть на формат занятий, на открытых уроках Otus регулярно разбирают REST, HTTP и работу с данными в живом эфире.
FAQ
Нужно ли писать charset=utf-8 в Content-Type для JSON?
Не обязательно: для application/json параметр charset не определен, а сам JSON между системами передается в UTF-8. Если клиент его прислал, сервер должен спокойно принять такой заголовок.
Чем JSON отличается от JSON5 и JSONC?
Это надстройки для конфигурационных файлов, где разрешены комментарии, а в JSON5 еще и одиночные кавычки и лишние запятые. Стандартные JSON.parse и json.loads такие файлы не читают, в HTTP-API их не используют.
Что такое JSONP и нужен ли он сейчас?
JSONP — старый прием обхода политики одного источника: данные заворачивались в вызов функции и загружались тегом <script>, то есть исполнялись как код. Сейчас для запросов к другому домену используют CORS, а JSONP считается небезопасным наследием.
Как быть, если сервер отдает JSON с Content-Type: text/html?
Это ошибка конфигурации сервера: клиенты и браузеры будут обрабатывать ответ как HTML. Правильно исправить заголовок на сервере; на клиенте проверять тип ответа до вызова res.json().



