PHPFlowPHP, MySQL, Laravel и JavaScript

JSON в PHP: json_encode и json_decode с примерами

Обновлено:

Коротко

Для работы с JSON в PHP есть две функции: json_encode() превращает массив или объект в строку JSON, а json_decode() разбирает строку обратно. Чтобы кириллица не превращалась в \u041f, добавьте флаг JSON_UNESCAPED_UNICODE, а для надёжности всегда передавайте JSON_THROW_ON_ERROR: тогда ошибка не потеряется молча.

Содержание
  1. json_encode: массив в JSON
  2. Кириллица в json_encode: JSON_UNESCAPED_UNICODE
  3. Полезные флаги json_encode
  4. json_decode: JSON в массив или объект
  5. Ошибки JSON и как их поймать
  6. Как отдать JSON в ответе: header и код ответа
  7. Как принять JSON из запроса
  8. JSON из внешнего API через cURL
  9. Дробные числа и точность
  10. Объекты в JSON: JsonSerializable
  11. Чтение и запись JSON-файла
  12. JSON в Laravel и быстрая проверка в консоли

json_encode: массив в JSON

Функция json_encode() принимает почти любое значение PHP: массив, объект, строку, число, true, false или null, и возвращает строку. Это основной способ отдать данные в JavaScript или во внешний API, и с него начинается любая работа с PHP JSON.

$user = [
    'id'     => 7,
    'name'   => 'Анна',
    'email'  => 'anna@example.com',
    'roles'  => ['admin', 'editor'],
    'active' => true,
];

echo json_encode($user, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
// {"id":7,"name":"Анна","email":"anna@example.com","roles":["admin","editor"],"active":true}
Пример json_encode с флагами JSON_UNESCAPED_UNICODE и JSON_THROW_ON_ERROR
Массив PHP превращается в строку JSON функцией json_encode с флагами для кириллицы и исключений

Как PHP решает, чем станет массив в JSON:

  • список с ключами 0, 1, 2... по порядку превращается в массив JSON [...];
  • массив со строковыми ключами или с "дырами" в нумерации превращается в объект {...};
  • объект PHP становится объектом JSON, но попадают в него только публичные свойства.

Подробнее о том, как устроены ключи и списки, рассказано в статье про массивы в PHP.

Кириллица в json_encode: JSON_UNESCAPED_UNICODE

По умолчанию json_encode() записывает все не-ASCII символы последовательностями \uXXXX. Для машины это корректный JSON, но читать такие строки и логи невозможно, а размер ответа растёт в несколько раз.

$data = ['city' => 'Москва'];

echo json_encode($data);
// {"city":"\u041c\u043e\u0441\u043a\u0432\u0430"}

echo json_encode($data, JSON_UNESCAPED_UNICODE);
// {"city":"Москва"}

Флаг работает только с текстом в UTF-8. Если строки пришли из старой базы в windows-1251, json_encode() вернёт false с ошибкой "Malformed UTF-8 characters". Перекодируйте данные заранее:

$name = mb_convert_encoding($rowFromOldDb['name'], 'UTF-8', 'Windows-1251');

Если кодировку исправить нельзя, флаг JSON_INVALID_UTF8_SUBSTITUTE заменит битые байты символом "�", и функция хотя бы вернёт результат.

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

Флаги объединяются через |. Набор ниже покрывает большинство задач:

ФлагЧто делаетКогда нужен
JSON_UNESCAPED_UNICODEОставляет русские буквы как естьПочти всегда
JSON_UNESCAPED_SLASHESНе экранирует / в адресахСсылки и пути в данных
JSON_PRETTY_PRINTПереносы строк и отступы в 4 пробелаКонфиги, отладка, файлы
JSON_THROW_ON_ERRORБросает JsonException вместо возврата falseВсегда
JSON_FORCE_OBJECTЛюбой массив пишет как объектAPI ждёт только объекты
JSON_PRESERVE_ZERO_FRACTIONПишет 10.0, а не 10Важен тип числа
JSON_NUMERIC_CHECKСтроки-числа превращает в числаОсторожно: "007" станет 7
const JSON_FLAGS = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR;

$config = ['site' => 'https://example.com/', 'title' => 'Мой сайт'];
echo json_encode($config, JSON_FLAGS | JSON_PRETTY_PRINT);
Красиво отформатированный JSON в консоли
Результат с флагами JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES и JSON_UNESCAPED_UNICODE

json_decode: JSON в массив или объект

Функция json_decode() разбирает строку. По умолчанию она возвращает объекты stdClass, а со вторым аргументом true - обычные ассоциативные массивы, с которыми привычнее работать.

$json = '{"id":7,"name":"Анна","roles":["admin","editor"],"address":{"city":"Москва"}}';

// объект: доступ через стрелку
$obj = json_decode($json, false, 512, JSON_THROW_ON_ERROR);
echo $obj->address->city; // Москва

// массив: доступ через квадратные скобки
$arr = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
echo $arr['address']['city']; // Москва
echo count($arr['roles']);    // 2
  • третий аргумент - максимальная глубина вложенности, по умолчанию 512;
  • флаг JSON_BIGINT_AS_STRING сохраняет очень большие числа (например, идентификаторы из внешних API) строкой, иначе они превратятся в float и потеряют точность;
  • JSON null и ошибка разбора без флага исключений дают одинаковый результат null, поэтому без JSON_THROW_ON_ERROR их не различить.
Схема преобразования JSON в массив PHP и обратно
Путь данных: строка JSON разбирается в массив, обрабатывается и снова кодируется в JSON

Ошибки JSON и как их поймать

Без флага исключений функции молча возвращают false или null. Узнать причину можно через json_last_error_msg(), но проще и надёжнее ловить JsonException:

$raw = '{"name": "Анна",}'; // лишняя запятая

try {
    $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    error_log('Плохой JSON: ' . $e->getMessage()); // Syntax error
    $data = [];
}
СообщениеПричинаКак исправить
Syntax errorЛишняя запятая, одинарные кавычки, комментарийИсправить источник, JSON допускает только двойные кавычки
Malformed UTF-8 charactersСтрока не в UTF-8Перекодировать через mb_convert_encoding()
Maximum stack depth exceededВложенность больше лимитаУвеличить третий аргумент
Control character errorПеревод строки или табуляция внутри строкиЭкранировать на стороне источника
Inf and NaN cannot be JSON encodedВ данных INF или NANЗаменить на null до кодирования

Частая ловушка: в начале строки стоит невидимый BOM от редактора, и разбор падает с "Syntax error", хотя JSON выглядит правильно. Уберите его: $raw = preg_replace('/^\xEF\xBB\xBF/', '', $raw);.

Проверка без разбора: json_validate

С PHP 8.3 есть функция json_validate(). Она только отвечает, корректна ли строка, и тратит меньше памяти, чем полный разбор. Подходит, когда JSON нужно просто проверить и сохранить как есть:

if (!json_validate($payload)) {
    http_response_code(422);
    exit('Некорректный JSON');
}

Как отдать JSON в ответе: header и код ответа

Чтобы браузер и JavaScript поняли, что пришёл JSON, нужен заголовок Content-Type: application/json. Это и есть запрос "php header json". Заголовок отправляется до любого вывода, иначе PHP выдаст предупреждение "Cannot modify header information".

function sendJson(mixed $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_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
    exit;
}

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false || $id === null) {
    sendJson(['error' => 'Не передан id'], 400);
}

sendJson(['id' => $id, 'status' => 'ok']);

Если вместо JSON браузер получает пустой ответ или HTML с текстом ошибки, включите вывод ошибок PHP или посмотрите лог: часто в ответ попадает предупреждение, которое ломает разбор на стороне JavaScript.

Заголовок Content-Type application/json в инструментах браузера
Во вкладке "Сеть" инструментов разработчика видно, что сервер отдал JSON с правильным заголовком

Как принять JSON из запроса

Когда фронтенд отправляет данные через fetch() с телом JSON, массив $_POST остаётся пустым: PHP заполняет его только для форм. Тело запроса читают из потока php://input:

$raw = file_get_contents('php://input');

try {
    $input = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
    sendJson(['error' => 'Ожидался JSON'], 400);
}

$title = trim((string) ($input['title'] ?? ''));

Код на стороне браузера для такого обработчика:

const response = await fetch('/api/notes.php', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'Купить молоко' })
});
const result = await response.json();
console.log(result.status);

Полный пример таблицы, которая получает данные с сервера в JSON и листает их постранично, разобран в статье про DataTables server-side processing.

JSON из внешнего API через cURL

Ответ чужого сервиса тоже приходит строкой JSON. Перед разбором проверьте код ответа: при ошибке многие API отдают HTML-страницу, и json_decode() на ней споткнётся.

$ch = curl_init('https://api.example.com/v1/rates?base=RUB');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 10,
    CURLOPT_HTTPHEADER     => ['Accept: application/json'],
]);
$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

if ($body === false || $status !== 200) {
    throw new RuntimeException("API ответил кодом {$status}");
}

$rates = json_decode($body, true, 512, JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING);
echo $rates['data']['USD'] ?? 'нет курса';

Отправка JSON в API выглядит зеркально: тело собирают через json_encode(), передают в CURLOPT_POSTFIELDS и добавляют заголовок Content-Type: application/json. Функцию curl_close() в PHP 8 вызывать не нужно, дескриптор закрывается сам.

Дробные числа и точность

Иногда в JSON вместо 0.1 появляется 0.10000000000000001. За это отвечает директива serialize_precision в php.ini: значение -1 (оно стоит по умолчанию) выводит самое короткое точное представление числа. Если в старом конфиге там 17, верните -1. Для денег лучше хранить копейки целым числом или передавать сумму строкой.

Объекты в JSON: JsonSerializable

Объект с приватными свойствами превратится в пустой {}. Чтобы управлять результатом, реализуйте интерфейс JsonSerializable:

final class Product implements JsonSerializable
{
    public function __construct(
        private int $id,
        private string $title,
        private int $priceKop,
    ) {}

    public function jsonSerialize(): array
    {
        return [
            'id'    => $this->id,
            'title' => $this->title,
            'price' => $this->priceKop / 100,
        ];
    }
}

echo json_encode([new Product(1, 'Чайник', 249900)], JSON_UNESCAPED_UNICODE);
// [{"id":1,"title":"Чайник","price":2499}]

Пустой массив PHP кодируется как []. Если API ждёт пустой объект, передайте new stdClass() или (object) []. А если после array_filter() список вдруг стал объектом с ключами "0", "2", "5", пропустите его через array_values().

Чтение и запись JSON-файла

JSON удобен для небольших настроек и кеша. Запись с блокировкой не даст двум запросам испортить файл одновременно:

$file = __DIR__ . '/storage/settings.json';

// запись
$settings = ['theme' => 'dark', 'perPage' => 20];
file_put_contents(
    $file,
    json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
    LOCK_EX
);

// чтение
$settings = is_file($file)
    ? json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR)
    : [];

Для данных, которые часто меняются и ищутся, файл не подходит, их лучше хранить в базе. Как сохранять и читать записи из MySQL, показано в статье PDO в PHP. В MySQL 8 есть и отдельный тип столбца JSON: значение из json_encode() записывается в него как обычная строка через подготовленный запрос.

JSON в Laravel и быстрая проверка в консоли

В Laravel ответ в JSON отдаёт хелпер response()->json(), он сам ставит заголовок. Русские буквы он по умолчанию экранирует, поэтому флаг передают четвёртым аргументом:

return response()->json($users, 200, [], JSON_UNESCAPED_UNICODE);

Вместо онлайн-сервисов для json decode удобно проверить строку прямо в терминале: так данные не уходят на чужой сайт.

php -r 'var_dump(json_decode(file_get_contents("data.json"), true));'

# красиво отформатировать файл
php -r 'echo json_encode(json_decode(file_get_contents("data.json")), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);'
Проверка json_decode в консоли с флагом исключений и без него
Без JSON_THROW_ON_ERROR ошибка разбора выглядит как обычный NULL, с флагом сразу видна причина

Частые вопросы

Как в PHP преобразовать массив в JSON?

Вызовите json_encode($array, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR). Список с ключами 0, 1, 2 станет массивом JSON, ассоциативный массив станет объектом.

Почему json_encode выводит русские символы как \u0430?

Так работает экранирование по умолчанию. Добавьте флаг JSON_UNESCAPED_UNICODE, и кириллица останется читаемой. Строки при этом должны быть в UTF-8.

Как получить из json_decode массив, а не объект?

Передайте вторым аргументом true: json_decode($json, true). Тогда вложенные объекты тоже станут ассоциативными массивами.

Почему json_decode возвращает null?

Строка некорректна: лишняя запятая, одинарные кавычки, BOM в начале или кодировка не UTF-8. Добавьте флаг JSON_THROW_ON_ERROR и прочитайте текст исключения.

Какой header нужен для ответа JSON в PHP?
header('Content-Type: application/json; charset=utf-8'), отправленный до любого вывода. Код ответа задайте через http_response_code().