JSON в PHP

JSON - это язык дипломатии между фронтом и бэком. Иногда, конечно, это дипломатия уровня «прислали строку вместо числа», но всё же. Сам формат - это объекты-«словари» и массивы (см. урок про массивы); подробное описание формата - в JSON-кросс-уроке.

json_encode - из PHP в JSON

<?php
$data = [
    'ok' => true,
    'user' => ['id' => 1, 'name' => 'Иван'],
];

echo json_encode($data);
// {"ok":true,"user":{"id":1,"name":"Иван"}}

Без флагов кириллица превращается в \uXXXX. Добавляем флаги:

<?php
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
// {
//     "ok": true,
//     "user": {
//         "id": 1,
//         "name": "Иван"
//     }
// }

Полезные флаги json_encode

<?php
$flags = JSON_UNESCAPED_UNICODE  // кириллица как есть
       | JSON_UNESCAPED_SLASHES  // / без экранирования
       | JSON_PRETTY_PRINT       // форматированный вывод (для отладки)
       | JSON_THROW_ON_ERROR;    // исключение вместо молчаливого false

echo json_encode($data, $flags);
`json_encode([])` вернёт `[]` (JSON-массив). Но `json_encode((object)[])` вернёт `{}` (JSON-объект). Если API ожидает объект - приводи к `(object)`.

json_decode - из JSON в PHP

<?php
$json = '{'id':1,'name':'Иван','scores':[95,88,72]}';

// Второй аргумент true - вернуть массив (по умолчанию - объект)
$data = json_decode($json, true);
echo $data['name'];       // Иван
echo $data['scores'][0];  // 95

// Без true - вернёт stdClass
$obj = json_decode($json);
echo $obj->name;       // Иван
echo $obj->scores[0];  // 95
Если JSON битый - `json_decode` вернёт `null`. При этом `null` - допустимое JSON-значение. Надёжнее всего использовать `JSON_THROW_ON_ERROR`.

Обработка ошибок

Старый способ (до PHP 7.3):

<?php
$data = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
    echo 'Ошибка: ' . json_last_error_msg();
}

Современный способ (PHP 7.3+) - через исключения:

<?php
try {
    $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
    echo 'Плохой JSON: ' . $e->getMessage();
}

Используй JSON_THROW_ON_ERROR - это чище и надёжнее. Работает и в json_encode, и в json_decode.

Отправка JSON-ответа

Типичный паттерн для API-эндпоинта (полный REST с правильными статус-кодами - в уроке REST API):

<?php
// Устанавливаем заголовок ДО вывода
header('Content-Type: application/json; charset=utf-8');

$response = [
    'ok' => true,
    'data' => [
        ['id' => 1, 'title' => 'Купить молоко', 'done' => false],
        ['id' => 2, 'title' => 'Написать код', 'done' => true],
    ],
];

echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
exit;

Для ошибок:

<?php
header('Content-Type: application/json; charset=utf-8');
http_response_code(400);

echo json_encode([
    'ok' => false,
    'error' => 'Параметр id обязателен',
], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
exit;

Чтение JSON из тела запроса

Когда фронтенд отправляет JSON через fetch или axios:

<?php
// Читаем сырое тело запроса
$rawBody = file_get_contents('php://input');

try {
    $data = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
    http_response_code(400);
    echo json_encode(['error' => 'Невалидный JSON']);
    exit;
}

// Теперь $data - обычный PHP-массив
$title = $data['title'] ?? '';
`$_POST` работает только с `application/x-www-form-urlencoded` и `multipart/form-data`. Если фронт шлёт `Content-Type: application/json`, данные попадают в `php://input`, а `$_POST` будет пустым.

Работа с JSON-файлами

Чтение конфигурации из файла:

<?php
$configJson = file_get_contents(__DIR__ . '/config.json');
$config = json_decode($configJson, true, 512, JSON_THROW_ON_ERROR);

echo $config['database']['host']; // localhost

Запись в файл:

<?php
$data = ['users' => [['id' => 1, 'name' => 'Иван']]];

$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
file_put_contents(__DIR__ . '/data.json', $json);

Вложенные данные и глубина

Третий аргумент json_decode - максимальная глубина вложенности (по умолчанию 512):

<?php
// Если JSON очень глубоко вложен
$deep = json_decode($json, true, 10, JSON_THROW_ON_ERROR);
// JsonException если вложенность > 10 уровней

Типичные подводные камни

<?php
// 1. Числа в строках
$json = '{'price':'199'}';
$data = json_decode($json, true);
echo $data['price'] + 1; // 200 - PHP приведёт, но лучше явно: (int)$data['price']

// 2. Null vs отсутствие ключа
$json = '{'name':null}';
$data = json_decode($json, true);
isset($data['name']);             // false! (значение null)
array_key_exists('name', $data);  // true

// 3. json_encode не принимает невалидный UTF-8
$bad = "Привет \x80 мир";              // битый UTF-8
json_encode($bad);                      // false!
json_encode($bad, JSON_THROW_ON_ERROR); // JsonException

Хелпер-функции

В реальных проектах удобно сделать обёртки:

<?php
declare(strict_types=1);

function jsonResponse(array $data, int $status = 200): never {
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
    exit;
}

function jsonError(string $message, int $status = 400): never {
    jsonResponse(['ok' => false, 'error' => $message], $status);
}

// Использование:
// jsonResponse(['ok' => true, 'users' => $users]);
// jsonError('Пользователь не найден', 404);

Типичные ошибки

  • Забыл JSON_THROW_ON_ERROR. json_decode молча возвращает null при битом JSON, и null неотличим от валидного "null". Всегда передавай флаг или проверяй через json_last_error().
  • json_encode без JSON_UNESCAPED_UNICODE. Кириллица превращается в При... - валидно, но нечитаемо в логах и раздувает payload вдвое. Добавляй флаг во все API-ответы.
  • Путаница assoc=true vs false. Без второго аргумента json_decode возвращает stdClass, и $data['name'] падает с ошибкой. Для API-данных всегда передавай true - массивы предсказуемее.
  • Кодирование объектов с приватными полями. json_encode видит только публичные свойства, приватные молча выпадают. Реализуй JsonSerializable::jsonSerialize() или используй DTO с публичными полями (или Symfony Serializer с groups).
  • Загрузка большого JSON через json_decode. Файл на 500 МБ съест всю память процесса. Для больших потоков используй halaxa/json-machine или pcrov/jsonreader - они парсят итеративно.

Best practices

  • Всегда передавай JSON_THROW_ON_ERROR в json_encode и json_decode. Молчаливые false/null - главный источник багов в API.
  • В API-ответах ставь JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES. Меньше байтов в сети и читаемые логи.
  • Для контроллеров используй Symfony\Component\HttpFoundation\JsonResponse или Symfony Serializer - они сами проставят Content-Type, кодировку и обработают ошибки.
  • DTO с публичными типизированными свойствами вместо stdClass. IDE подсказывает поля, статанализ ловит опечатки, JsonSerializable даёт контроль над форматом.
  • Ограничивай глубину третьим аргументом json_decode($json, true, 32, JSON_THROW_ON_ERROR). Защита от DoS-атак с глубоко вложенным payload.

Мини-задание

  • Сформируй JSON для списка задач (id, title, done) и выведи с JSON_PRETTY_PRINT
  • Распарсь JSON обратно и выведи только невыполненные задачи (done === false)
  • Напиши эндпоинт, который читает JSON из php://input и возвращает JSON-ответ
  • Добавь обработку ошибок через JSON_THROW_ON_ERROR и try/catch
  • Создай функцию jsonResponse() и используй её для ответов API

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.