JSON: формат обмена, а не «дамп PHP-переменной»

JSON в PHP — это основной формат для обмена структурированными данными между бэкендом, браузером, мобильным приложением, очередью или внешним API. Он текстовый, переносимый и не привязан к PHP-классам. Поэтому его удобно отдавать наружу через HTTP, хранить в логах, подписывать, передавать между сервисами и читать без PHP-рантайма.

Главная граница такая: json_encode() превращает PHP-значение в JSON-строку, json_decode() превращает JSON-строку обратно в PHP-значение. Это не то же самое, что serialize(): JSON описывает данные, а PHP serialization описывает PHP-структуру со всеми локальными деталями языка.

flowchart TD A[PHP-значение: array, object, scalar] --> B[json_encode] B --> C[JSON-строка UTF-8] C --> D[HTTP API, лог, очередь, файл] D --> E[json_decode] E --> F[PHP array или stdClass] G[serialize] --> H[PHP-specific байтовая строка] H --> I[Только доверенное внутреннее хранение] I --> J[unserialize] J --> K[PHP-значение] C -. переносимый формат .-> D H -. не публичный API .-> I
flowchart TD
    A[PHP-значение: array, object, scalar] --> B[json_encode]
    B --> C[JSON-строка UTF-8]
    C --> D[HTTP API, лог, очередь, файл]
    D --> E[json_decode]
    E --> F[PHP array или stdClass]
    G[serialize] --> H[PHP-specific байтовая строка]
    H --> I[Только доверенное внутреннее хранение]
    I --> J[unserialize]
    J --> K[PHP-значение]
    C -. переносимый формат .-> D
    H -. не публичный API .-> I
JSON подходит для обмена данными между системами; `serialize()` — PHP-специфичный механизм для доверенного внутреннего состояния.
<?php

$payload = [
    'id' => 42,
    'title' => 'Словарь PHP',
    'published' => true,
    'tags' => ['php', 'json', 'api'],
];

$json = json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);

echo $json . PHP_EOL;

На выходе будет нормальная JSON-строка с русским текстом без \uXXXX-экранирования:

{"id":42,"title":"Словарь PHP","published":true,"tags":["php","json","api"]}
Quick recall
Почему JSON в PHP не стоит воспринимать как «дамп PHP-переменной»?

json_encode(): массив, объект и UTF-8

json_encode() принимает почти любое PHP-значение, кроме resource, и возвращает строку JSON или false, если не включён режим исключений. Для реального кода почти всегда лучше включать JSON_THROW_ON_ERROR, чтобы ошибка кодирования не потерялась среди обычных значений. Это тот же стиль мышления, что и в Ошибки, исключения и Throwable: ошибка должна быть явной.

<?php

try {
    $json = json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
} catch (JsonException $e) {
    // Логируем техническую причину, наружу отдаём безопасное сообщение.
    throw new RuntimeException('Не удалось подготовить JSON-ответ', previous: $e);
}

Все строки для JSON должны быть в UTF-8. Если в массив случайно попали байты в другой кодировке, кодирование может упасть. Это напрямую связано со статьёй Строки, UTF-8 и mbstring: PHP-строка сама по себе — набор байтов, а JSON требует понятной Unicode-модели.

Важная деталь из Массивы как ordered map: PHP array может быть списком или ассоциативной картой. В JSON это разные формы: список становится [], ассоциативный массив — {}. Если у списка пропали последовательные ключи, он тоже может стать объектом.

<?php

echo json_encode(['a', 'b', 'c'], JSON_THROW_ON_ERROR) . PHP_EOL;
// ["a","b","c"]

$items = ['a', 'b', 'c'];
unset($items[1]);

echo json_encode($items, JSON_THROW_ON_ERROR) . PHP_EOL;
// {"0":"a","2":"c"}

echo json_encode(array_values($items), JSON_THROW_ON_ERROR) . PHP_EOL;
// ["a","c"]

Если вы отдаёте API-массив элементов, после фильтрации часто нужен array_values(), иначе клиент внезапно получит object вместо array.

Quick recall
Что произойдёт с ошибкой `json_encode()`, если включить `JSON_THROW_ON_ERROR`?

json_decode(): array или stdClass

json_decode() по умолчанию превращает JSON objects в stdClass. Если вторым аргументом передать true, объекты станут ассоциативными массивами. Для большинства прикладного PHP-кода, где вы валидируете вход и перекладываете его в DTO или обычный массив, true читается проще.

<?php

$raw = '{"id":42,"title":"Словарь PHP"}';

$asObject = json_decode($raw, false, 512, JSON_THROW_ON_ERROR);
echo $asObject->title . PHP_EOL;

$asArray = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
echo $asArray['title'] . PHP_EOL;

Параметр depth ограничивает вложенность. Не ставьте его огромным «на всякий случай» для пользовательского ввода: глубоко вложенный JSON может быть способом потратить память и CPU. Если данные пришли из php://input, это уже тема веб-границы из GET, POST и фильтрация ввода и SAPI и суперглобалы: сначала читаем тело запроса, потом декодируем, потом валидируем структуру.

<?php

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

try {
    $input = json_decode($body, true, 64, JSON_THROW_ON_ERROR);
} catch (JsonException) {
    http_response_code(400);
    echo json_encode(['error' => 'Некорректный JSON'], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
    return;
}

if (!is_array($input) || !isset($input['email']) || !is_string($input['email'])) {
    http_response_code(422);
    echo json_encode(['error' => 'Некорректные поля'], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
    return;
}

json_validate() появился в PHP 8.3. Он полезен, если нужно только проверить синтаксис JSON и не строить массив или объект в памяти. Но не надо вызывать json_validate() прямо перед json_decode(): декодирование и так проверяет синтаксис, иначе вы просто парсите одну строку два раза.

Quick recall
Что вернёт `json_decode($raw)` для JSON object по умолчанию, если не передать `true` вторым аргументом?

Числа, строки и флаги

JSON различает string, number, boolean, null, array и object. Но PHP и внешние системы могут по-разному понимать границы чисел. Большие идентификаторы, номера заказов, банковские BIN, телефоны и ZIP-коды лучше хранить строками, даже если они выглядят как числа.

<?php

$json = '{"order_id":12345678901234567890}';

var_dump(json_decode($json, true, 512, JSON_THROW_ON_ERROR));
var_dump(json_decode($json, true, 512, JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING));

JSON_BIGINT_AS_STRING помогает не потерять точность при декодировании больших integer. А вот JSON_NUMERIC_CHECK при кодировании стоит использовать очень осторожно: он превращает числоподобные строки в numbers. Телефон "+77001234567", артикул "00123" или внешний id могут испортиться.

<?php

$data = [
    'phone' => '+77001234567',
    'sku' => '00123',
];

echo json_encode($data, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK) . PHP_EOL;
// {"phone":77001234567,"sku":123}

Для денежных сумм не полагайтесь на float в JSON как на бухгалтерский тип. Часто безопаснее передавать сумму в минимальных единицах (amount_cents: 1299) или строкой с явно оговорённым форматом ("12.99").

Для дат правило из Дата и время остаётся тем же: отдавайте машинный формат вроде RFC 3339, а не локализованную строку «15 июня 2026 г.».

<?php

$createdAt = new DateTimeImmutable('2026-06-15 12:30:00', new DateTimeZone('UTC'));

echo json_encode([
    'created_at' => $createdAt->format(DateTimeInterface::RFC3339_EXTENDED),
], JSON_THROW_ON_ERROR);

JsonSerializable: явная форма объекта

Если передать объект в json_encode(), по умолчанию попадут только публичные свойства. Это редко хорошая модель для доменного объекта: можно случайно отдать лишнее или, наоборот, получить пустой {}. Лучше явно определить форму JSON через JsonSerializable или собрать массив в отдельном методе.

<?php

final class UserProfile implements JsonSerializable
{
    public function __construct(
        private int $id,
        private string $name,
        private string $email,
    ) {}

    public function jsonSerialize(): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            // email намеренно не отдаём в публичный JSON
        ];
    }
}

echo json_encode(new UserProfile(7, 'Анна', 'anna@example.com'), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);

Это особенно важно рядом с XSS, экранирование вывода и шаблоны: JSON-кодирование не делает данные «безопасными для любого места». JSON внутри <script>, HTML-атрибута, URL и HTTP-ответа — разные контексты вывода.

serialize() и unserialize(): только для доверенной PHP-среды

serialize() создаёт PHP-специфичное байтовое представление значения. Оно умеет сохранять типы, массивы, ссылки и некоторые объекты. Это может быть нормально для внутреннего кеша, сессий или временного состояния, которое читает тот же PHP-код той же версии приложения.

<?php

$state = ['page' => 3, 'filters' => ['active' => true]];
$stored = serialize($state);

var_dump($stored);
var_dump(unserialize($stored, ['allowed_classes' => false]));

Но unserialize() нельзя применять к недоверенному пользовательскому вводу. Даже allowed_classes не превращает его в безопасный формат обмена: при десериализации объектов возможны автозагрузка классов, вызовы __wakeup() / __unserialize() и неприятные цепочки в легаси-коде. Для данных, которые уходят наружу или приходят от клиента, используйте JSON и валидируйте схему на своей стороне.

Если вам надо защитить внутренне сохранённую serialized-строку от подмены, минимум — подписывайте её через HMAC и проверяйте подпись до unserialize(). Но это всё ещё внутренний PHP-механизм, не публичный API.

Практические правила

Используйте JSON_THROW_ON_ERROR почти всегда. Для русскоязычных и вообще Unicode-данных добавляйте JSON_UNESCAPED_UNICODE, если нет требования хранить ASCII-only. Для API явно решайте, где объект, где список, и не забывайте array_values() после фильтрации списков.

Не включайте JSON_NUMERIC_CHECK глобально. Большие числа декодируйте с JSON_BIGINT_AS_STRING, если точность важна. Даты отдавайте строками RFC 3339, деньги — строками или integer в минимальных единицах. serialize() оставляйте для доверенной внутренней PHP-инфраструктуры, а не для cookie, URL, внешних webhook и публичных API.

См. также

Sources

  1. PHP Manual: JSON
  2. PHP Manual: json_encode
  3. PHP Manual: json_decode
  4. PHP Manual: json_validate
  5. PHP Manual: JsonException
  6. PHP Manual: serialize
  7. PHP Manual: unserialize
  8. RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format