Отладка API на коленке: почему ваш JSON возвращается с ошибкой?
Вы когда-нибудь сидели в три часа ночи, пялились в пустой экран Postman и думали: «Почему мой PHP-скрипт отдаёт HTML вместо JSON?»? Если да — вы не одиноки. Тема Returning JSON from a PHP Script на Stack Overflow собрала десятки тысяч просмотров, и это не случайно. Каждый второй начинающий backend-разработчик наступает на одни и те же грабли: забытые заголовки, мусорный вывод, неверный Content-Type.
В этой статье мы не просто скопируем ответ с форума. Мы разберём, как возвращать JSON из PHP правильно, быстро и без сюрпризов. Вы узнаете, почему exit(json_encode($data)) может вас подвести, как сэкономить миллисекунды на сериализации и что делать, если JSON вдруг «сломался».
Готовы? Поехали.
Анатомия правильного JSON-ответа
Начнём с базы. Вот как не надо делать:
<?php
$data = ['status' => 'ok', 'message' => 'Привет!'];
echo json_encode($data);
?>
Почему это плохо? Во-первых, нет заголовка Content-Type. Браузер или клиент могут воспринять ответ как HTML или текст. Во-вторых, если до этого в коде был какой-то пробел, echo или варнинг — JSON сломается.
Правильный минимальный шаблон выглядит так:
<?php
header('Content-Type: application/json; charset=utf-8');
$data = ['status' => 'ok', 'message' => 'Привет!'];
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
?>
Обратите внимание на JSON_UNESCAPED_UNICODE — он сохраняет кириллицу читаемой, а не превращает её в \u043F\u0440\u0438\u0432\u0435\u0442. И exit — страховка от случайного вывода после ответа.
Заголовки: не только Content-Type
Для оптимизации производительности JSON добавьте ещё пару заголовков:
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
header('Cache-Control: no-cache, must-revalidate');
X-Content-Type-Options: nosniff запрещает браузеру «угадывать» тип контента — это безопасность. А Cache-Control полезен для динамических данных, чтобы клиент не кэшировал устаревший ответ.
Обработка ошибок: когда JSON_ENCODE падает
Функция json_encode возвращает false при неудаче. И вот тут начинается самое интересное. Если вы просто сделаете echo json_encode($data), а в ответ получите пустую строку — клиент уйдёт в недоумение.
Всегда проверяйте результат:
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
if ($json === false) {
http_response_code(500);
echo json_encode(['error' => 'JSON encoding failed: ' . json_last_error_msg()]);
exit;
}
echo $json;
exit;
Кстати, json_last_error_msg() вернёт понятную строку: «Malformed UTF-8 characters» или «Inf and NaN cannot be JSON encoded». Это спасёт часы дебага.
Глубоко вложенные данные и рекурсия
Ещё одна проблема — циклические ссылки в объектах. Если вы передаёте объект с взаимными ссылками, json_encode выбросит ошибку. Решение: использовать JSON_PARTIAL_OUTPUT_ON_ERROR или предварительно преобразовать объект в массив.
$json = json_encode($complexObject, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR);
Но будьте осторожны: частичный вывод может быть неполным. Лучше чинить структуру данных.
Оптимизация производительности JSON в PHP
Теперь перейдём к самому вкусному — как ускорить возврат JSON из PHP.
1. Кэширование сериализованного JSON
Если данные меняются редко (справочники, конфиги, список городов), не сериализуйте их каждый раз. Сохраните готовый JSON в файл, Memcached или Redis.
// Пример с файлом
$cacheFile = __DIR__ . '/cache/data.json';
if (file_exists($cacheFile) && time() - filemtime($cacheFile) < 3600) {
$json = file_get_contents($cacheFile);
} else {
$data = fetchExpensiveData(); // SQL-запрос или API
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
file_put_contents($cacheFile, $json, LOCK_EX);
}
header('Content-Type: application/json; charset=utf-8');
echo $json;
exit;
Это даст прирост скорости в десятки раз, особенно если данные собираются из нескольких таблиц.
2. Опции json_encode, которые экономят время
Флаг JSON_UNESCAPED_SLASHES ускоряет сериализацию URL, а JSON_NUMERIC_CHECK преобразует числовые строки в числа — меньше данных для передачи.
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);
Но не злоупотребляйте: JSON_NUMERIC_CHECK может «испортить» строки вида «01234» (обрежет ведущий ноль).
3. Сжатие на лету
Если JSON большой (сотни килобайт), включите gzip-сжатие. PHP делает это автоматически, если в .htaccess или конфиге сервера прописано:
// Включение сжатия в PHP
ini_set('zlib.output_compression', 'On');
// или через ob_gzhandler
ob_start('ob_gzhandler');
Размер ответа уменьшится в 5-10 раз, а время передачи сократится.
Практический пример: полноценный JSON endpoint
Соберём всё вместе. Допустим, у нас есть API, которое возвращает список пользователей.
<?php
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
header('Cache-Control: no-cache, must-revalidate');
// Включаем сжатие
ob_start('ob_gzhandler');
// Функция для безопасного JSON-ответа
function sendJsonResponse($data, int $statusCode = 200): void {
http_response_code($statusCode);
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json === false) {
http_response_code(500);
$json = json_encode(['error' => 'Internal server error: ' . json_last_error_msg()]);
}
echo $json;
ob_end_flush();
exit;
}
// Пример: получаем данные из БД
try {
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass');
$stmt = $pdo->query('SELECT id, name