В модулях Битрикс JSON встречается на каждом шагу: ответ REST-контроллера, тело для HttpClient, кеш настроек, очередь задач, обмен с Bitrix24. Писать везде «голый» json_encode/json_decode с разными флагами — быстрый путь к тихим false, кракозябрам в кириллице и двойному экранированию. Нормальный контракт ядра — Bitrix\Main\Web\Json.
Рядом по теме: HttpClient, Application, Context, Request, Result и Error, REST API в D7. Ниже — encode/decode, опции, обработка ошибок и типичные грабли модулей на проде.
Зачем Json, а не json_encode напрямую
json_encode в PHP молча возвращает false при ошибке (невалидный UTF-8, рекурсия, INF/NAN). В логах это выглядит как «пустой ответ», а на фронте — 500 без понятного текста. Bitrix\Main\Web\Json:
- задаёт единый набор флагов по умолчанию (Unicode без
\uXXXX, экранирование опасных символов для HTML-контекста); - при ошибке encode/decode бросает исключение, а не отдаёт
false/null; - одинаково используется в ядре, модулях и вашем
local/— меньше зоопарка флагов.
Для разового скрипта CLI json_encode допустим. В модуле и API — берите Json.
Минимальный encode и decode
Класс статический, отдельный объект не создаёте:
<?php
use Bitrix\Main\Web\Json;
use Bitrix\Main\ArgumentException;
$payload = [
'orderId' => 10542,
'status' => 'paid',
'items' => [
['sku' => 'A-100', 'qty' => 2, 'title' => 'Наушники'],
],
];
try {
$json = Json::encode($payload);
// {"orderId":10542,"status":"paid","items":[{"sku":"A-100","qty":2,"title":"Наушники"}]}
} catch (ArgumentException $e) {
// невалидные данные / кодировка — не глотайте исключение молча
throw $e;
}
try {
$data = Json::decode($json); // ассоциативный массив по умолчанию
$orderId = (int)$data['orderId'];
} catch (ArgumentException $e) {
// битый JSON от внешней системы
AddMessage2Log($e->getMessage(), 'local.json');
}
Второй аргумент decode — $assoc (по умолчанию true). Если нужны объекты stdClass, передайте false. В модулях почти всегда удобнее массив.
Опции encode: дефолт ядра и свои флаги
Если второй аргумент encode не передан, ядро использует свой набор (в актуальных версиях main — комбинация с JSON_UNESCAPED_UNICODE и HEX-экранированием <, &, кавычек). Это безопасно, когда JSON попадает в HTML/шаблон, и читаемо для кириллицы.
Когда нужен «красивый» дамп для отладки или файлового экспорта:
<?php
use Bitrix\Main\Web\Json;
$pretty = Json::encode(
$payload,
JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
);
// Компактный вариант без HEX (если JSON уходит только в HTTP-тело, не в HTML):
$compact = Json::encode(
$payload,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
Не смешивайте в одном проекте три стиля флагов «как получилось». Выберите контракт модуля (например, всегда Unicode + unescaped slashes для API) и держитесь его. Для ответа браузеру/шаблону оставляйте дефолт ядра или явный HEX.
Ответ HTTP: Content-Type и тело
Encode сам по себе заголовки не ставит. Для ручного endpoint или ajax-обработчика:
<?php
use Bitrix\Main\Application;
use Bitrix\Main\Web\Json;
$app = Application::getInstance();
$response = $app->getContext()->getResponse();
$data = ['ok' => true, 'id' => 42];
$response->addHeader('Content-Type', 'application/json; charset=UTF-8');
$response->flush(Json::encode($data));
// либо в старом стиле ajax:
// header('Content-Type: application/json; charset=UTF-8');
// echo Json::encode($data);
// die();
В контроллерах Engine (REST/маршруты D7) массив из action сериализуется ядром — там не дублируйте echo Json::encode, иначе получите двойную обёртку. Ручной Json::encode нужен, когда вы сами формируете тело (кастомный ajax, вебхук наружу, файл).
Подробнее про Response и заголовки — в статье про Application и Context.
Связка с HttpClient
Исходящий POST JSON — классика. Тело строкой, заголовок явно:
<?php
use Bitrix\Main\Web\HttpClient;
use Bitrix\Main\Web\Json;
use Bitrix\Main\ArgumentException;
$http = new HttpClient([
'socketTimeout' => 5,
'streamTimeout' => 15,
]);
$http->setHeader('Content-Type', 'application/json; charset=UTF-8');
$http->setHeader('Accept', 'application/json');
$payload = ['event' => 'order.paid', 'orderId' => 10542];
try {
$body = $http->post(
'https://erp.example/api/hooks',
Json::encode($payload)
);
} catch (ArgumentException $e) {
// не смогли даже сериализовать — внешний вызов не начинаем
throw $e;
}
if ($body === false) {
throw new \RuntimeException('HTTP transport: ' . implode('; ', $http->getError()));
}
$status = $http->getStatus();
if ($status < 200 || $status >= 300) {
throw new \RuntimeException('HTTP status=' . $status . ' body=' . mb_substr((string)$body, 0, 500));
}
try {
$result = Json::decode((string)$body);
} catch (ArgumentException $e) {
throw new \RuntimeException('Invalid JSON from ERP: ' . $e->getMessage());
}
Не передавайте PHP-массив в post() при Content-Type: application/json — клиент уйдёт в form-urlencoded. Полный разбор таймаутов и статусов — в гайде по HttpClient.
UTF-8 и «кракозябры»
JSON по спецификации — Unicode. Типичные проблемы на проектах Битрикс:
- строка из БД/файла в windows-1251 попала в encode — PHP 8+ чаще падает с ошибкой, на старых версиях мог получиться
false; - двойной encode: уже JSON-строку снова прогнали через
Json::encode— в теле появляются\"и лишний уровень кавычек; - ответ отдали с
Content-Type: text/htmlбез charset — браузер «угадал» кодировку; - в лог писали сырой binary/emoji без truncate — журнал раздулся.
Практичный хелпер перед encode, если источник «грязный»:
<?php
use Bitrix\Main\Web\Json;
use Bitrix\Main\Text\Encoding;
function toUtf8Deep($value)
{
if (is_array($value)) {
foreach ($value as $k => $v) {
$value[$k] = toUtf8Deep($v);
}
return $value;
}
if (is_string($value) && !mb_check_encoding($value, 'UTF-8')) {
return Encoding::convertEncoding($value, SITE_CHARSET, 'UTF-8');
}
return $value;
}
$json = Json::encode(toUtf8Deep($payload));
На чистом UTF-8 проекте (современный Битрикс) хелпер обычно не нужен — важнее не смешивать charset при чтении внешних CSV/XML.
Ошибки encode/decode: не глотать false
Антипример, который до сих пор копируют из старых сниппетов:
<?php
// Плохо: тихий провал
$json = json_encode($data);
file_put_contents($path, $json); // может записать пустую строку/false
$data = json_decode($raw, true);
if (!$data) {
// пустой массив [] тоже «ложный» — логика ломается
}
Нормальный путь с Json + явным контрактом ошибок (удобно стыковать с Result/Error):
<?php
use Bitrix\Main\Result;
use Bitrix\Main\Error;
use Bitrix\Main\Web\Json;
use Bitrix\Main\ArgumentException;
function decodeApiBody(string $raw): Result
{
$result = new Result();
if ($raw === '') {
$result->addError(new Error('Empty body'));
return $result;
}
try {
$data = Json::decode($raw);
$result->setData(is_array($data) ? $data : ['value' => $data]);
} catch (ArgumentException $e) {
$result->addError(new Error('JSON: ' . $e->getMessage()));
}
return $result;
}
Пустой объект {} и пустой массив [] — валидный JSON. Не проверяйте успех через if (!$data).
Большие payload и память
Json::encode/decode держат всё в памяти. Для выгрузки каталога на десятки тысяч позиций:
- не собирайте один гигантский массив «все товары» — отдавайте чанками / NDJSON / потоковую запись;
- лимит
memory_limitна агентах часто ниже, чем на хитах FPM — тестируйте на проде-подобных объёмах; - в лог никогда не пишите полный JSON заказа с ПДн:
mb_substr($json, 0, 500)или только id/status; - если JSON кладёте в Option или кеш — следите за размером: Option не файловое хранилище.
Тело запроса: получить JSON из Request
Входящий webhook или свой ajax часто присылает raw JSON. Не читайте php://input вразнобой — через Context это предсказуемее:
<?php
use Bitrix\Main\Application;
use Bitrix\Main\Web\Json;
use Bitrix\Main\ArgumentException;
$request = Application::getInstance()->getContext()->getRequest();
$raw = $request->getInput(); // тело как строка
if ($raw === '' || $raw === null) {
// иногда клиент шлёт form — тогда смотрите getPostList / getJsonList в вашей версии
http_response_code(400);
echo Json::encode(['error' => 'empty_body']);
return;
}
try {
$data = Json::decode($raw);
} catch (ArgumentException $e) {
http_response_code(400);
echo Json::encode(['error' => 'invalid_json', 'message' => $e->getMessage()]);
return;
}
// дальше валидация полей, не «доверие к массиву»
$event = (string)($data['event'] ?? '');
$orderId = (int)($data['orderId'] ?? 0);
Проверяйте Content-Type на входе, если endpoint публичный: неожиданный multipart/form-data при ожидании JSON — повод вернуть 415, а не пытаться «угадать» парсер. CSRF для браузерных сессий — отдельно (bitrix_sessid / Signed); для server-to-server webhook обычно токен в заголовке.
JSON в файле и в Option
Иногда конфиг или снимок состояния кладут в файл. Encode + атомарная запись:
<?php
use Bitrix\Main\Web\Json;
use Bitrix\Main\IO\File;
use Bitrix\Main\IO\Directory;
$dir = $_SERVER['DOCUMENT_ROOT'] . '/upload/local_export/';
if (!Directory::isDirectoryExists($dir)) {
Directory::createDirectory($dir);
}
$path = $dir . 'orders-snapshot.json';
$tmp = $path . '.tmp';
$json = Json::encode($snapshot, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
File::putFileContents($tmp, $json);
rename($tmp, $path); // атомарнее, чем писать сразу в целевой файл
Чтение обратно — File::getFileContents + Json::decode в try/catch. Для небольших настроек модуля чаще достаточно Option::set со строкой JSON, но помните лимиты размера Option и то, что это не очередь и не файловое хранилище логов. Крупные структуры лучше в файл, highload-блок или свою таблицу.
Связанный разбор работы с путями — в статье про Main\IO; настройки модуля — в Option и Configuration.
Типичные ошибки модулей на проде
- Тихий
false. Остались наjson_encodeбез проверкиjson_last_error(). Переходите наJson::encodeи ловите исключение. - Двойной encode. Сначала
Json::encode($arr), потом эту строку кладут в массив и encode снова — API получает строку вместо объекта. - Двойной decode. Engine уже отдал массив, разработчик ещё раз
Json::decode«на всякий». - Неверный Content-Type. Тело JSON, а заголовок
text/htmlили form-urlencoded — клиенты и CDN ведут себя сюрпризно. - Массив в HttpClient при JSON Content-Type. Уходит не то тело.
- Проверка
if (!$data)после decode. Ломает валидные[]и0/falseв JSON. - Разные флаги Unicode в соседних сервисах — то кириллица читаемая, то
\u0410, то сломанный diff в git. - Логируют секреты. JSON с token/password целиком в
AddMessage2Log.
PHP json_encode с JSON_THROW_ON_ERROR — чем отличается
В PHP 7.3+ можно писать json_encode($data, JSON_THROW_ON_ERROR) и ловить JsonException. Это лучше тихого false, но в проекте на Битрикс всё равно предпочтительнее Bitrix\Main\Web\Json: те же исключения в стиле ядра (ArgumentException), единые дефолтные флаги и меньше сюрпризов при code review («почему тут throw-флаг, а тремя файлами ниже — нет»). Оставляйте нативный json_* для изолированных CLI-утилит вне runtime Битрикс.
Быстрая отладка
<?php
use Bitrix\Main\Web\Json;
use Bitrix\Main\ArgumentException;
function debugJson(string $label, $value): void
{
try {
if (is_string($value)) {
$decoded = Json::decode($value);
AddMessage2Log([
'label' => $label,
'kind' => 'string-json',
'preview' => mb_substr($value, 0, 400),
'keys' => is_array($decoded) ? array_keys($decoded) : gettype($decoded),
], 'local.json');
} else {
$json = Json::encode($value);
AddMessage2Log([
'label' => $label,
'kind' => 'php-value',
'preview' => mb_substr($json, 0, 400),
], 'local.json');
}
} catch (ArgumentException $e) {
AddMessage2Log(['label' => $label, 'error' => $e->getMessage()], 'local.json');
}
}
На стейдже этого достаточно. На проде после разбора инцидента отключайте — журнал /upload/ быстро растёт.
Краткий чеклист
- В модуле —
Bitrix\Main\Web\Json, не россыпьjson_encodeс разными флагами. - Ошибки encode/decode обрабатывайте через try/catch или Result, не через
if (!$data). - Для HTTP: явный
Content-Type: application/json; charset=UTF-8+ строка изJson::encode. - Исходящие вызовы — через HttpClient с таймаутами.
- Не логируйте полные payload с ПДн и токенами; режьте preview.
- Большие выгрузки — чанками, не одним encode на весь каталог.
Вывод
Bitrix\Main\Web\Json — тонкая, но важная обёртка: единые флаги, исключения вместо тихого false, понятный контракт для модулей. Связка «Json сериализует → HttpClient отправляет → Result описывает исход» закрывает большинство интеграционных кейсов без сюрпризов на проде. Адреса собирайте через Uri, HTTP-контекст берите из Application/Context, ошибки API оформляйте через Result/Error.

