Когда приложение просит у базы данные, ему часто нужен не набор отдельных колонок, а готовый JSON.
Например:
- отдать пользователя в ответ API;
- положить событие в очередь;
- записать понятный лог;
- собрать вложенный объект с заказами, платежами или настройками;
- быстро превратить строку таблицы в документ.
Можно, конечно, собрать JSON руками в приложении. Но тогда начинаются вечные мелочи: где поставить кавычки, как экранировать текст, что делать с датами, как не превратить число в строку, как аккуратно обработать NULL.
В PostgreSQL для этого есть функция to_jsonb.
Она берёт SQL-значение и превращает его в значение типа jsonb.
Это может быть:
- обычное число;
- строка;
- дата;
- булево значение;
- массив;
- целая строка таблицы.
Главная идея простая:
to_jsonb превращает SQL-данные в нормальный JSONB без ручной склейки текста.
Что делает to_jsonb
Синтаксис очень простой:
to_jsonb(value)
На вход передаём одно значение, на выходе получаем jsonb.
Посмотрим на простые значения:
SELECT
to_jsonb(42) AS num,
to_jsonb('hello'::text) AS str,
to_jsonb(true) AS flag,
to_jsonb(ARRAY[1, 2, 3]) AS arr;
Результат будет таким по смыслу:
{
"num": 42,
"str": "hello",
"flag": true,
"arr": [1, 2, 3]
}
Каждое значение превращается в естественную JSON-форму:
| SQL-значение |
JSON-значение |
42 |
42 |
'hello'::text |
"hello" |
true |
true |
ARRAY[1, 2, 3] |
[1, 2, 3] |
NULL::int |
null |
Важно: to_jsonb не делает из всего строки. Число остаётся числом, булево значение остаётся булевым, а SQL-массив становится JSON-массивом.
Зачем это нужно
Представьте таблицу users:
id |
email |
name |
country |
created_at |
| 7 |
a@example.com |
Anna |
DE |
2026-01-10 08:00:00 |
Если приложению нужен JSON-объект пользователя, можно не перечислять все поля вручную. Можно передать в to_jsonb всю строку.
SELECT to_jsonb(u) AS user_json
FROM users u
WHERE u.id = 7;
Результат:
{
"id": 7,
"email": "a@example.com",
"name": "Anna",
"country": "DE",
"created_at": "2026-01-10T08:00:00"
}
Здесь важный момент: u внутри to_jsonb(u) — это не одна колонка. Это вся строка таблицы users с псевдонимом u.
PostgreSQL берёт имена колонок и делает их ключами JSON-объекта.
То есть:
- колонка
id стала ключом "id";
- колонка
email стала ключом "email";
- колонка
country стала ключом "country".
Это очень удобно, когда форма JSON должна примерно повторять форму таблицы.
Типы не теряются
Одна из главных причин использовать to_jsonb — сохранение типов.
Плохой способ собрать JSON — склеивать текст руками:
SELECT
'{"id": ' || id || ', "active": "' || active || '"}' AS payload
FROM users;
Такой подход быстро ломается:
- текст нужно экранировать;
- кавычки легко забыть;
- булево значение можно случайно превратить в строку;
NULL может испортить всю строку;
- даты начинают жить своей жизнью.
to_jsonb делает это аккуратно.
SELECT
jsonb_typeof(to_jsonb(o.amount)) AS amount_type,
jsonb_typeof(to_jsonb(o.status)) AS status_type,
jsonb_typeof(to_jsonb(o.paid)) AS paid_type
FROM orders o
LIMIT 1;
Результат может быть таким:
amount_type |
status_type |
paid_type |
| number |
string |
boolean |
Если amount — число, в JSON оно будет числом. Если paid — булево значение, в JSON оно будет true или false, а не строкой "true".
Это особенно важно для фронтенда и API. Клиент не должен угадывать, почему сумма пришла строкой, а флаг активности — текстом.
Что происходит с NULL
SQL-значение NULL превращается в JSON null.
SELECT to_jsonb(NULL::int) AS value;
Результат:
null
Если NULL находится внутри строки таблицы, ключ останется, а значение будет null.
Например, есть пользователь без имени:
Запрос:
SELECT to_jsonb(u) AS user_json
FROM users u
WHERE u.id = 10;
Результат:
{
"id": 10,
"email": "user@example.com",
"name": null
}
Это честный JSON null, а не пустая строка и не текст "null".
Даты и время
Даты и время при преобразовании в JSON становятся строками.
SELECT
to_jsonb(DATE '2026-01-10') AS day_value,
to_jsonb(TIMESTAMP '2026-01-10 08:30:00') AS time_value;
Результат будет примерно таким:
{
"day_value": "2026-01-10",
"time_value": "2026-01-10T08:30:00"
}
Здесь есть важная тонкость.
timestamp без часового пояса не содержит информации о зоне. Это просто дата и время. Если клиент решит, что такое значение всегда в UTC, он может ошибиться на несколько часов.
Если в проекте важно строго работать с UTC, лучше заранее договориться о типах и правилах хранения времени. Часто для событий используют timestamptz, а на уровне API явно фиксируют формат времени.
Превращаем SQL-массив в JSON-массив
to_jsonb умеет работать и с массивами PostgreSQL.
SELECT to_jsonb(ARRAY['new', 'vip', 'paid']) AS tags;
Результат:
["new", "vip", "paid"]
Массив чисел станет массивом чисел:
SELECT to_jsonb(ARRAY[10, 20, 30]) AS values_json;
Результат:
[10, 20, 30]
Но помните: обычный SQL-массив должен быть однородным. В нём элементы должны быть одного типа или приводиться к общему типу.
Если нужно собрать JSON-массив из разных типов, лучше использовать jsonb_build_array.
SELECT jsonb_build_array(1, 'active', true) AS payload;
Результат:
[1, "active", true]
А to_jsonb(ARRAY[...]) оставьте для случаев, когда у вас уже есть нормальный SQL-массив.
Как убрать лишние поля из JSON
Иногда хочется превратить строку в JSON, но не отдавать все колонки.
Например, в таблице users есть служебные поля:
password_hash;
internal_note;
created_at.
Если сделать просто to_jsonb(u), они тоже попадут в JSON.
Для jsonb есть удобный оператор -, который удаляет ключ из объекта.
SELECT to_jsonb(u) - 'password_hash' AS public_user
FROM users u;
Можно удалить несколько ключей цепочкой:
SELECT to_jsonb(u) - 'password_hash' - 'internal_note' AS public_user
FROM users u;
Или передать массив ключей:
SELECT to_jsonb(u) - '{password_hash,internal_note}'::text[] AS public_user
FROM users u;
Такой подход удобен, когда почти вся строка подходит для ответа, но несколько полей нужно скрыть.
Как добавить новое поле к JSON
Раз результат to_jsonb имеет тип jsonb, его можно объединять с другим jsonb через оператор ||.
Например, возьмём пользователя и добавим вычисляемое поле label.
SELECT
to_jsonb(u) || jsonb_build_object(
'label',
u.name || ' <' || u.email || '>'
) AS user_card
FROM users u;
Результат:
{
"id": 7,
"email": "a@example.com",
"name": "Anna",
"country": "DE",
"label": "Anna <a@example.com>"
}
Здесь:
to_jsonb(u) делает объект из строки пользователя.
jsonb_build_object(...) создаёт дополнительный объект.
|| склеивает два объекта в один.
Если в обоих объектах есть одинаковый ключ, значение справа перезапишет значение слева.
Например:
SELECT '{"status": "old"}'::jsonb || '{"status": "new"}'::jsonb AS result;
Результат:
{
"status": "new"
}
Это полезно, когда нужно переопределить или добавить несколько полей поверх исходной строки.
Вложенные объекты: пользователь с заказами
Настоящая польза to_jsonb начинается, когда нужно собрать вложенный JSON.
Допустим, есть таблицы:
Нужно вернуть пользователя и массив его заказов.
SELECT
to_jsonb(u) || jsonb_build_object(
'orders',
(
SELECT jsonb_agg(to_jsonb(o))
FROM orders o
WHERE o.user_id = u.id
)
) AS user_with_orders
FROM users u
WHERE u.id = 7;
Результат:
{
"id": 7,
"email": "a@example.com",
"name": "Anna",
"country": "DE",
"orders": [
{
"id": 100,
"user_id": 7,
"amount": 49.90,
"status": "paid"
},
{
"id": 101,
"user_id": 7,
"amount": 12.00,
"status": "pending"
}
]
}
Разберём по частям.
Внешняя часть:
to_jsonb(u)
делает JSON-объект пользователя.
Внутренний подзапрос:
SELECT jsonb_agg(to_jsonb(o))
FROM orders o
WHERE o.user_id = u.id
делает массив заказов. Каждый заказ превращается в JSON через to_jsonb(o), а jsonb_agg собирает их в массив.
Потом jsonb_build_object создаёт объект с ключом orders, а оператор || добавляет этот ключ к пользователю.
Что делать, если связанных строк нет
Есть нюанс: если jsonb_agg не нашёл ни одной строки, он вернёт NULL, а не пустой массив.
Для API часто удобнее вернуть [].
Для этого используют coalesce.
SELECT
to_jsonb(u) || jsonb_build_object(
'orders',
coalesce(
(
SELECT jsonb_agg(to_jsonb(o))
FROM orders o
WHERE o.user_id = u.id
),
'[]'::jsonb
)
) AS user_with_orders
FROM users u
WHERE u.id = 7;
Теперь пользователь без заказов получит:
{
"id": 7,
"email": "a@example.com",
"name": "Anna",
"country": "DE",
"orders": []
}
Это приятнее для клиента: он всегда видит массив и может спокойно по нему проходиться.
to_jsonb против jsonb_build_object
В PostgreSQL есть ещё одна популярная функция — jsonb_build_object.
Она собирает объект из пар ключ-значение.
SELECT jsonb_build_object(
'user_id', u.id,
'email', u.email,
'is_local', u.country = 'DE'
) AS user_card
FROM users u;
Результат:
{
"user_id": 7,
"email": "a@example.com",
"is_local": true
}
Когда выбирать to_jsonb, а когда jsonb_build_object?
Используйте to_jsonb, когда нужен объект примерно в форме строки таблицы:
SELECT to_jsonb(u)
FROM users u;
Используйте jsonb_build_object, когда форму ответа нужно собрать вручную:
- переименовать ключи;
- выбрать только несколько полей;
- добавить вычисляемые значения;
- сделать стабильный внешний контракт, не зависящий от структуры таблицы.
Например:
SELECT jsonb_build_object(
'id', u.id,
'display_name', u.name,
'country_code', u.country
) AS public_user
FROM users u;
На практике эти функции часто используют вместе:
SELECT
(to_jsonb(u) - 'password_hash') || jsonb_build_object(
'orders_count',
(
SELECT COUNT(*)
FROM orders o
WHERE o.user_id = u.id
)
) AS user_summary
FROM users u;
Здесь мы берём почти всю строку пользователя, удаляем секретное поле и добавляем вычисленное количество заказов.
Почему не стоит собирать JSON руками
Иногда новичок пытается сделать JSON через конкатенацию строк.
Примерно так:
SELECT
'{"id": ' || id || ', "email": "' || email || '"}' AS user_json
FROM users;
На маленьких данных это может выглядеть рабочим. Но потом появляются проблемы:
- в email или имени может быть кавычка;
- текст нужно правильно экранировать;
NULL ломает склейку;
- числа и булевы значения легко превратить в строки;
- дату можно отдать в неожиданном формате;
- вложенные объекты становятся мучением.
Лучше поручить сериализацию PostgreSQL:
SELECT jsonb_build_object(
'id', id,
'email', email
) AS user_json
FROM users;
Или, если нужна вся строка:
SELECT to_jsonb(u) AS user_json
FROM users u;
Так запрос получается и короче, и надёжнее.
to_jsonb против row_to_json
В PostgreSQL есть старая функция row_to_json. Она тоже превращает строку в JSON.
SELECT row_to_json(u) AS user_json
FROM users u;
Похоже на to_jsonb(u), но результат другого типа.
row_to_json возвращает json;
to_jsonb возвращает jsonb.
Разница важная.
Тип json хранит JSON ближе к исходному тексту. Тип jsonb хранит разобранное нормализованное представление, с которым удобнее работать в запросах.
С jsonb доступны полезные операторы:
SELECT
to_jsonb(u) - 'password_hash' AS public_user
FROM users u;
Можно объединять объекты:
SELECT
to_jsonb(u) || jsonb_build_object('source', 'api') AS payload
FROM users u;
Можно проверять наличие структуры, использовать JSONB-операторы и индексы.
Поэтому в новом коде для PostgreSQL обычно удобнее выбирать to_jsonb, если дальше с результатом нужно что-то делать внутри базы.
row_to_json может пригодиться, если вам по какой-то причине нужен именно тип json, а не jsonb. Но для большинства практических задач jsonb гибче.
Чем json отличается от jsonb
Коротко:
| Тип |
Как устроен |
Когда полезен |
json |
хранит JSON как текст |
когда важен исходный текстовый вид |
jsonb |
хранит разобранное значение |
когда нужно удобно искать, менять, сравнивать и индексировать |
У jsonb есть важные особенности:
- пробелы исходного JSON не сохраняются;
- порядок ключей объекта не стоит считать значимым;
- дубли ключей в объекте нормализуются;
- зато появляются удобные операторы и индексация.
Для рабочих запросов, фильтров, обновлений и сборки ответов обычно выбирают jsonb.
Мини-шпаргалка
| Задача |
Подход |
| Превратить число, строку или дату в JSONB |
to_jsonb(value) |
| Превратить всю строку таблицы в объект |
to_jsonb(alias) |
| Убрать лишний ключ |
to_jsonb(alias) - 'key' |
| Убрать несколько ключей |
to_jsonb(alias) - '{key1,key2}'::text[] |
| Добавить поле |
`to_jsonb(alias) |
| Собрать вложенный список строк |
jsonb_agg(to_jsonb(alias)) |
| Собрать объект вручную |
jsonb_build_object(...) |
Вернуть пустой массив вместо NULL |
coalesce(value, '[]'::jsonb) |
Пример: готовый JSON для API
Соберём реалистичный ответ: пользователь, публичные поля, количество заказов и список последних заказов.
SELECT
(to_jsonb(u) - '{password_hash,internal_note}'::text[])
|| jsonb_build_object(
'orders_count',
(
SELECT COUNT(*)
FROM orders o
WHERE o.user_id = u.id
),
'recent_orders',
coalesce(
(
SELECT jsonb_agg(to_jsonb(o) - 'user_id')
FROM orders o
WHERE o.user_id = u.id
),
'[]'::jsonb
)
) AS payload
FROM users u
WHERE u.id = 7;
Результат будет похож на такой:
{
"id": 7,
"email": "a@example.com",
"name": "Anna",
"country": "DE",
"created_at": "2026-01-10T08:00:00",
"orders_count": 2,
"recent_orders": [
{
"id": 100,
"amount": 49.90,
"status": "paid"
},
{
"id": 101,
"amount": 12.00,
"status": "pending"
}
]
}
В одном запросе мы:
- Превратили пользователя в JSON.
- Убрали закрытые поля.
- Добавили количество заказов.
- Добавили массив заказов.
- Убрали из заказов лишний
user_id.
- Подстраховались пустым массивом через
coalesce.
Это и есть сильная сторона to_jsonb: он хорошо работает как базовый строительный блок для JSON-ответов.
Как это выглядит в других СУБД
В MySQL чаще используют JSON_OBJECT.
SELECT JSON_OBJECT(
'id', id,
'email', email
) AS user_json
FROM users;
Но прямого полного аналога to_jsonb(u), где вся строка таблицы автоматически превращается в объект с именами колонок, в обычном стиле MySQL нет. Чаще приходится перечислять ключи вручную.
В ClickHouse подход другой. Там есть функции для сериализации, например toJSONString, и отдельные возможности для работы с JSON, но модель отличается от PostgreSQL. Запросы с to_jsonb, jsonb_agg и jsonb_build_object напрямую перенести не получится.
Если проект поддерживает несколько баз данных, сборку JSON лучше держать в отдельном слое и не размазывать по десяткам мест. Синтаксис и поведение JSON-функций заметно отличаются.
Частые ошибки
Думать, что to_jsonb(u) берёт одну колонку
В запросе:
SELECT to_jsonb(u)
FROM users u;
u — это вся строка таблицы с псевдонимом u, а не отдельная колонка.
Если нужна одна колонка, укажите её явно:
SELECT to_jsonb(u.email)
FROM users u;
Отдать наружу секретные поля
Если в таблице есть password_hash, token, internal_note или другие служебные поля, простой to_jsonb(u) включит их в результат.
Перед публичной выдачей лучше явно убрать лишнее:
SELECT to_jsonb(u) - '{password_hash,token,internal_note}'::text[]
FROM users u;
Забыть, что jsonb_agg может вернуть NULL
Если связанных строк нет, jsonb_agg возвращает NULL.
Для списков в API часто лучше так:
SELECT coalesce(jsonb_agg(to_jsonb(o)), '[]'::jsonb)
FROM orders o
WHERE o.user_id = 7;
Использовать ручную склейку вместо JSON-функций
Не стоит собирать JSON строками, если PostgreSQL может сделать это сам.
Плохо:
SELECT '{"id": ' || id || ', "email": "' || email || '"}'
FROM users;
Лучше:
SELECT jsonb_build_object('id', id, 'email', email)
FROM users;
Или:
SELECT to_jsonb(u)
FROM users u;
Ждать сохранения порядка ключей в jsonb
В JSON-объекте порядок ключей не должен быть важен. Особенно в jsonb.
Если клиент зависит от порядка ключей объекта, это плохой контракт. Для порядка используйте массивы, а для смысла — имена ключей.
Главное
to_jsonb — функция PostgreSQL, которая превращает SQL-значение в jsonb.
Она умеет преобразовывать:
- числа;
- строки;
- даты;
- булевы значения;
- SQL-массивы;
- целые строки таблиц.
Самое полезное применение — to_jsonb(alias), когда вся строка таблицы становится JSON-объектом с ключами по именам колонок.
Запомните основные правила:
to_jsonb сохраняет естественные JSON-типы;
- SQL-
NULL становится JSON null;
- строка таблицы превращается в объект;
- лишние ключи можно удалить оператором
-;
- новые поля можно добавить через
|| и jsonb_build_object;
- вложенные списки удобно собирать через
jsonb_agg(to_jsonb(...));
- для полностью ручной формы ответа используйте
jsonb_build_object;
- в новом коде чаще удобнее работать с
jsonb, чем с json.
Если совсем коротко: to_jsonb — это аккуратный способ сказать PostgreSQL: «Возьми это SQL-значение и преврати его в нормальный JSONB». Без ручной склейки, без охоты за кавычками и без потери типов.
Когда приложение просит у базы данные, ему часто нужен не набор отдельных колонок, а готовый JSON.
Например:
Можно, конечно, собрать JSON руками в приложении. Но тогда начинаются вечные мелочи: где поставить кавычки, как экранировать текст, что делать с датами, как не превратить число в строку, как аккуратно обработать
NULL.В PostgreSQL для этого есть функция
to_jsonb.Она берёт SQL-значение и превращает его в значение типа
jsonb.Это может быть:
Главная идея простая:
to_jsonbпревращает SQL-данные в нормальный JSONB без ручной склейки текста.Что делает
to_jsonbСинтаксис очень простой:
to_jsonb(value)На вход передаём одно значение, на выходе получаем
jsonb.Посмотрим на простые значения:
SELECT to_jsonb(42) AS num, to_jsonb('hello'::text) AS str, to_jsonb(true) AS flag, to_jsonb(ARRAY[1, 2, 3]) AS arr;Результат будет таким по смыслу:
{ "num": 42, "str": "hello", "flag": true, "arr": [1, 2, 3] }Каждое значение превращается в естественную JSON-форму:
4242'hello'::text"hello"truetrueARRAY[1, 2, 3][1, 2, 3]NULL::intnullВажно:
to_jsonbне делает из всего строки. Число остаётся числом, булево значение остаётся булевым, а SQL-массив становится JSON-массивом.Зачем это нужно
Представьте таблицу
users:idemailnamecountrycreated_atЕсли приложению нужен JSON-объект пользователя, можно не перечислять все поля вручную. Можно передать в
to_jsonbвсю строку.SELECT to_jsonb(u) AS user_json FROM users u WHERE u.id = 7;Результат:
{ "id": 7, "email": "a@example.com", "name": "Anna", "country": "DE", "created_at": "2026-01-10T08:00:00" }Здесь важный момент:
uвнутриto_jsonb(u)— это не одна колонка. Это вся строка таблицыusersс псевдонимомu.PostgreSQL берёт имена колонок и делает их ключами JSON-объекта.
То есть:
idстала ключом"id";emailстала ключом"email";countryстала ключом"country".Это очень удобно, когда форма JSON должна примерно повторять форму таблицы.
Типы не теряются
Одна из главных причин использовать
to_jsonb— сохранение типов.Плохой способ собрать JSON — склеивать текст руками:
SELECT '{"id": ' || id || ', "active": "' || active || '"}' AS payload FROM users;Такой подход быстро ломается:
NULLможет испортить всю строку;to_jsonbделает это аккуратно.SELECT jsonb_typeof(to_jsonb(o.amount)) AS amount_type, jsonb_typeof(to_jsonb(o.status)) AS status_type, jsonb_typeof(to_jsonb(o.paid)) AS paid_type FROM orders o LIMIT 1;Результат может быть таким:
amount_typestatus_typepaid_typeЕсли
amount— число, в JSON оно будет числом. Еслиpaid— булево значение, в JSON оно будетtrueилиfalse, а не строкой"true".Это особенно важно для фронтенда и API. Клиент не должен угадывать, почему сумма пришла строкой, а флаг активности — текстом.
Что происходит с
NULLSQL-значение
NULLпревращается в JSONnull.SELECT to_jsonb(NULL::int) AS value;Результат:
nullЕсли
NULLнаходится внутри строки таблицы, ключ останется, а значение будетnull.Например, есть пользователь без имени:
idemailnameNULLЗапрос:
SELECT to_jsonb(u) AS user_json FROM users u WHERE u.id = 10;Результат:
{ "id": 10, "email": "user@example.com", "name": null }Это честный JSON
null, а не пустая строка и не текст"null".Даты и время
Даты и время при преобразовании в JSON становятся строками.
SELECT to_jsonb(DATE '2026-01-10') AS day_value, to_jsonb(TIMESTAMP '2026-01-10 08:30:00') AS time_value;Результат будет примерно таким:
{ "day_value": "2026-01-10", "time_value": "2026-01-10T08:30:00" }Здесь есть важная тонкость.
timestampбез часового пояса не содержит информации о зоне. Это просто дата и время. Если клиент решит, что такое значение всегда в UTC, он может ошибиться на несколько часов.Если в проекте важно строго работать с UTC, лучше заранее договориться о типах и правилах хранения времени. Часто для событий используют
timestamptz, а на уровне API явно фиксируют формат времени.Превращаем SQL-массив в JSON-массив
to_jsonbумеет работать и с массивами PostgreSQL.SELECT to_jsonb(ARRAY['new', 'vip', 'paid']) AS tags;Результат:
["new", "vip", "paid"]Массив чисел станет массивом чисел:
SELECT to_jsonb(ARRAY[10, 20, 30]) AS values_json;Результат:
[10, 20, 30]Но помните: обычный SQL-массив должен быть однородным. В нём элементы должны быть одного типа или приводиться к общему типу.
Если нужно собрать JSON-массив из разных типов, лучше использовать
jsonb_build_array.SELECT jsonb_build_array(1, 'active', true) AS payload;Результат:
[1, "active", true]А
to_jsonb(ARRAY[...])оставьте для случаев, когда у вас уже есть нормальный SQL-массив.Как убрать лишние поля из JSON
Иногда хочется превратить строку в JSON, но не отдавать все колонки.
Например, в таблице
usersесть служебные поля:password_hash;internal_note;created_at.Если сделать просто
to_jsonb(u), они тоже попадут в JSON.Для
jsonbесть удобный оператор-, который удаляет ключ из объекта.SELECT to_jsonb(u) - 'password_hash' AS public_user FROM users u;Можно удалить несколько ключей цепочкой:
SELECT to_jsonb(u) - 'password_hash' - 'internal_note' AS public_user FROM users u;Или передать массив ключей:
SELECT to_jsonb(u) - '{password_hash,internal_note}'::text[] AS public_user FROM users u;Такой подход удобен, когда почти вся строка подходит для ответа, но несколько полей нужно скрыть.
Как добавить новое поле к JSON
Раз результат
to_jsonbимеет типjsonb, его можно объединять с другимjsonbчерез оператор||.Например, возьмём пользователя и добавим вычисляемое поле
label.SELECT to_jsonb(u) || jsonb_build_object( 'label', u.name || ' <' || u.email || '>' ) AS user_card FROM users u;Результат:
{ "id": 7, "email": "a@example.com", "name": "Anna", "country": "DE", "label": "Anna <a@example.com>" }Здесь:
to_jsonb(u)делает объект из строки пользователя.jsonb_build_object(...)создаёт дополнительный объект.||склеивает два объекта в один.Если в обоих объектах есть одинаковый ключ, значение справа перезапишет значение слева.
Например:
SELECT '{"status": "old"}'::jsonb || '{"status": "new"}'::jsonb AS result;Результат:
{ "status": "new" }Это полезно, когда нужно переопределить или добавить несколько полей поверх исходной строки.
Вложенные объекты: пользователь с заказами
Настоящая польза
to_jsonbначинается, когда нужно собрать вложенный JSON.Допустим, есть таблицы:
users;orders.Нужно вернуть пользователя и массив его заказов.
SELECT to_jsonb(u) || jsonb_build_object( 'orders', ( SELECT jsonb_agg(to_jsonb(o)) FROM orders o WHERE o.user_id = u.id ) ) AS user_with_orders FROM users u WHERE u.id = 7;Результат:
{ "id": 7, "email": "a@example.com", "name": "Anna", "country": "DE", "orders": [ { "id": 100, "user_id": 7, "amount": 49.90, "status": "paid" }, { "id": 101, "user_id": 7, "amount": 12.00, "status": "pending" } ] }Разберём по частям.
Внешняя часть:
делает JSON-объект пользователя.
Внутренний подзапрос:
SELECT jsonb_agg(to_jsonb(o)) FROM orders o WHERE o.user_id = u.idделает массив заказов. Каждый заказ превращается в JSON через
to_jsonb(o), аjsonb_aggсобирает их в массив.Потом
jsonb_build_objectсоздаёт объект с ключомorders, а оператор||добавляет этот ключ к пользователю.Что делать, если связанных строк нет
Есть нюанс: если
jsonb_aggне нашёл ни одной строки, он вернётNULL, а не пустой массив.Для API часто удобнее вернуть
[].Для этого используют
coalesce.SELECT to_jsonb(u) || jsonb_build_object( 'orders', coalesce( ( SELECT jsonb_agg(to_jsonb(o)) FROM orders o WHERE o.user_id = u.id ), '[]'::jsonb ) ) AS user_with_orders FROM users u WHERE u.id = 7;Теперь пользователь без заказов получит:
{ "id": 7, "email": "a@example.com", "name": "Anna", "country": "DE", "orders": [] }Это приятнее для клиента: он всегда видит массив и может спокойно по нему проходиться.
to_jsonbпротивjsonb_build_objectВ PostgreSQL есть ещё одна популярная функция —
jsonb_build_object.Она собирает объект из пар ключ-значение.
SELECT jsonb_build_object( 'user_id', u.id, 'email', u.email, 'is_local', u.country = 'DE' ) AS user_card FROM users u;Результат:
{ "user_id": 7, "email": "a@example.com", "is_local": true }Когда выбирать
to_jsonb, а когдаjsonb_build_object?Используйте
to_jsonb, когда нужен объект примерно в форме строки таблицы:SELECT to_jsonb(u) FROM users u;Используйте
jsonb_build_object, когда форму ответа нужно собрать вручную:Например:
SELECT jsonb_build_object( 'id', u.id, 'display_name', u.name, 'country_code', u.country ) AS public_user FROM users u;На практике эти функции часто используют вместе:
SELECT (to_jsonb(u) - 'password_hash') || jsonb_build_object( 'orders_count', ( SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id ) ) AS user_summary FROM users u;Здесь мы берём почти всю строку пользователя, удаляем секретное поле и добавляем вычисленное количество заказов.
Почему не стоит собирать JSON руками
Иногда новичок пытается сделать JSON через конкатенацию строк.
Примерно так:
SELECT '{"id": ' || id || ', "email": "' || email || '"}' AS user_json FROM users;На маленьких данных это может выглядеть рабочим. Но потом появляются проблемы:
NULLломает склейку;Лучше поручить сериализацию PostgreSQL:
SELECT jsonb_build_object( 'id', id, 'email', email ) AS user_json FROM users;Или, если нужна вся строка:
SELECT to_jsonb(u) AS user_json FROM users u;Так запрос получается и короче, и надёжнее.
to_jsonbпротивrow_to_jsonВ PostgreSQL есть старая функция
row_to_json. Она тоже превращает строку в JSON.SELECT row_to_json(u) AS user_json FROM users u;Похоже на
to_jsonb(u), но результат другого типа.row_to_jsonвозвращаетjson;to_jsonbвозвращаетjsonb.Разница важная.
Тип
jsonхранит JSON ближе к исходному тексту. Типjsonbхранит разобранное нормализованное представление, с которым удобнее работать в запросах.С
jsonbдоступны полезные операторы:SELECT to_jsonb(u) - 'password_hash' AS public_user FROM users u;Можно объединять объекты:
SELECT to_jsonb(u) || jsonb_build_object('source', 'api') AS payload FROM users u;Можно проверять наличие структуры, использовать JSONB-операторы и индексы.
Поэтому в новом коде для PostgreSQL обычно удобнее выбирать
to_jsonb, если дальше с результатом нужно что-то делать внутри базы.row_to_jsonможет пригодиться, если вам по какой-то причине нужен именно типjson, а неjsonb. Но для большинства практических задачjsonbгибче.Чем
jsonотличается отjsonbКоротко:
jsonjsonbУ
jsonbесть важные особенности:Для рабочих запросов, фильтров, обновлений и сборки ответов обычно выбирают
jsonb.Мини-шпаргалка
to_jsonb(value)to_jsonb(alias)to_jsonb(alias) - 'key'to_jsonb(alias) - '{key1,key2}'::text[]jsonb_agg(to_jsonb(alias))jsonb_build_object(...)NULLcoalesce(value, '[]'::jsonb)Пример: готовый JSON для API
Соберём реалистичный ответ: пользователь, публичные поля, количество заказов и список последних заказов.
SELECT (to_jsonb(u) - '{password_hash,internal_note}'::text[]) || jsonb_build_object( 'orders_count', ( SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id ), 'recent_orders', coalesce( ( SELECT jsonb_agg(to_jsonb(o) - 'user_id') FROM orders o WHERE o.user_id = u.id ), '[]'::jsonb ) ) AS payload FROM users u WHERE u.id = 7;Результат будет похож на такой:
{ "id": 7, "email": "a@example.com", "name": "Anna", "country": "DE", "created_at": "2026-01-10T08:00:00", "orders_count": 2, "recent_orders": [ { "id": 100, "amount": 49.90, "status": "paid" }, { "id": 101, "amount": 12.00, "status": "pending" } ] }В одном запросе мы:
user_id.coalesce.Это и есть сильная сторона
to_jsonb: он хорошо работает как базовый строительный блок для JSON-ответов.Как это выглядит в других СУБД
В MySQL чаще используют
JSON_OBJECT.SELECT JSON_OBJECT( 'id', id, 'email', email ) AS user_json FROM users;Но прямого полного аналога
to_jsonb(u), где вся строка таблицы автоматически превращается в объект с именами колонок, в обычном стиле MySQL нет. Чаще приходится перечислять ключи вручную.В ClickHouse подход другой. Там есть функции для сериализации, например
toJSONString, и отдельные возможности для работы с JSON, но модель отличается от PostgreSQL. Запросы сto_jsonb,jsonb_aggиjsonb_build_objectнапрямую перенести не получится.Если проект поддерживает несколько баз данных, сборку JSON лучше держать в отдельном слое и не размазывать по десяткам мест. Синтаксис и поведение JSON-функций заметно отличаются.
Частые ошибки
Думать, что
to_jsonb(u)берёт одну колонкуВ запросе:
SELECT to_jsonb(u) FROM users u;u— это вся строка таблицы с псевдонимомu, а не отдельная колонка.Если нужна одна колонка, укажите её явно:
SELECT to_jsonb(u.email) FROM users u;Отдать наружу секретные поля
Если в таблице есть
password_hash,token,internal_noteили другие служебные поля, простойto_jsonb(u)включит их в результат.Перед публичной выдачей лучше явно убрать лишнее:
SELECT to_jsonb(u) - '{password_hash,token,internal_note}'::text[] FROM users u;Забыть, что
jsonb_aggможет вернутьNULLЕсли связанных строк нет,
jsonb_aggвозвращаетNULL.Для списков в API часто лучше так:
SELECT coalesce(jsonb_agg(to_jsonb(o)), '[]'::jsonb) FROM orders o WHERE o.user_id = 7;Использовать ручную склейку вместо JSON-функций
Не стоит собирать JSON строками, если PostgreSQL может сделать это сам.
Плохо:
SELECT '{"id": ' || id || ', "email": "' || email || '"}' FROM users;Лучше:
SELECT jsonb_build_object('id', id, 'email', email) FROM users;Или:
SELECT to_jsonb(u) FROM users u;Ждать сохранения порядка ключей в
jsonbВ JSON-объекте порядок ключей не должен быть важен. Особенно в
jsonb.Если клиент зависит от порядка ключей объекта, это плохой контракт. Для порядка используйте массивы, а для смысла — имена ключей.
Главное
to_jsonb— функция PostgreSQL, которая превращает SQL-значение вjsonb.Она умеет преобразовывать:
Самое полезное применение —
to_jsonb(alias), когда вся строка таблицы становится JSON-объектом с ключами по именам колонок.Запомните основные правила:
to_jsonbсохраняет естественные JSON-типы;NULLстановится JSONnull;-;||иjsonb_build_object;jsonb_agg(to_jsonb(...));jsonb_build_object;jsonb, чем сjson.Если совсем коротко:
to_jsonb— это аккуратный способ сказать PostgreSQL: «Возьми это SQL-значение и преврати его в нормальный JSONB». Без ручной склейки, без охоты за кавычками и без потери типов.