jsonb_build_object — это функция PostgreSQL, которая собирает JSON-объект прямо внутри запроса.
Она принимает аргументы парами:
key, value, key, value, key, value
На нечётных позициях стоят ключи, на чётных — значения.
Например:
SELECT jsonb_build_object(
'id', 1,
'email', 'ann@example.com',
'is_active', true
) AS payload;
Результат:
{"id": 1, "email": "ann@example.com", "is_active": true}
Эта функция особенно полезна, когда нужно отдать из базы готовый JSON для API, отчёта, webhook-события или вложенной структуры. Вместо того чтобы собирать JSON строками в приложении, можно описать нужный ответ прямо в SELECT.
Главное преимущество: PostgreSQL сам правильно сохраняет типы, расставляет кавычки, экранирует строки и превращает SQL-значения в корректный JSON.
Зачем нужен jsonb_build_object
Представим, что у нас есть таблица пользователей.
id | email | country | is_active
---+-----------------+---------+----------
1 | ann@example.com | DE | true
2 | bob@example.com | BR | false
Мы хотим получить не обычные колонки, а JSON-объект для каждой строки.
SELECT jsonb_build_object(
'id', id,
'email', email,
'country', country,
'is_active', is_active
) AS user_json
FROM users;
Результат будет примерно таким:
{"id": 1, "email": "ann@example.com", "country": "DE", "is_active": true}
{"id": 2, "email": "bob@example.com", "country": "BR", "is_active": false}
Обратите внимание: id остался числом, is_active остался булевым значением, а строки получили кавычки.
Если бы мы собирали JSON руками через конкатенацию строк, пришлось бы самостоятельно следить за кавычками, экранированием и типами. Это неудобно и опасно.
Плохой стиль выглядит примерно так:
SELECT
'{"id": "' || id || '", "email": "' || email || '"}' AS payload
FROM users;
На первый взгляд работает. Но здесь id уже стал строкой, потому что попал в кавычки. Если в email окажется кавычка или другой специальный символ, JSON может сломаться. А если значение будет NULL, результат тоже может стать неожиданным.
С jsonb_build_object PostgreSQL делает эту работу за вас.
Базовый синтаксис
Форма функции простая:
jsonb_build_object(key, value, key, value, ...)
Пример:
SELECT jsonb_build_object(
'user_id', u.id,
'email', u.email,
'country', u.country
) AS payload
FROM users u;
Здесь:
'user_id', 'email', 'country' — ключи будущего JSON-объекта;
u.id, u.email, u.country — значения;
- результат имеет тип
jsonb.
Ключи PostgreSQL приводит к тексту. Значения могут быть почти любого типа: числа, строки, даты, булевы значения, NULL, массивы, другие JSON-объекты.
Аргументов должно быть чётное число
У jsonb_build_object каждый ключ должен иметь значение. Поэтому число аргументов обязательно должно быть чётным.
Так правильно:
SELECT jsonb_build_object(
'id', 1,
'name', 'Ann'
) AS payload;
А так неправильно:
SELECT jsonb_build_object(
'id', 1,
'name'
) AS payload;
Во втором запросе для ключа 'name' нет значения. PostgreSQL выдаст ошибку.
Логика простая: функция читает аргументы парами. Если пара не закончилась, объект собрать невозможно.
Значения сохраняют типы
Одна из главных причин использовать jsonb_build_object — сохранение типов.
SELECT jsonb_build_object(
'id', 10,
'amount', 1499.50,
'is_paid', true,
'comment', NULL
) AS payload;
Результат:
{"id": 10, "amount": 1499.50, "is_paid": true, "comment": null}
Здесь важно:
10 — число, а не строка "10";
1499.50 — число, а не строка "1499.50";
true — JSON-булево значение, а не строка "true";
- SQL-значение
NULL превратилось в JSON null.
Это удобно для фронтенда и внешних API. Клиенту не нужно гадать, почему цена пришла строкой, а флаг активности пришёл текстом.
Что происходит с NULL
Если NULL стоит на месте значения, ключ остаётся в объекте, а значение становится JSON null.
SELECT jsonb_build_object(
'id', 1,
'middle_name', NULL
) AS payload;
Результат:
{"id": 1, "middle_name": null}
Это нормальное поведение. Ключ не исчезает.
Если вы хотите убрать поля со значением null, можно дополнительно использовать jsonb_strip_nulls.
SELECT jsonb_strip_nulls(
jsonb_build_object(
'id', 1,
'middle_name', NULL
)
) AS payload;
Результат:
{"id": 1}
А вот NULL на месте ключа использовать нельзя.
SELECT jsonb_build_object(
NULL, 'Ann'
) AS payload;
Ключ JSON-объекта должен существовать. Поэтому такой запрос завершится ошибкой.
Пример: карточка пользователя для API
Самый частый сценарий — собрать ответ для API прямо в запросе.
Допустим, нам нужна карточка пользователя:
{
"user_id": 1,
"name": "Ann",
"is_verified": true,
"signup_year": 2024
}
Соберём её в SQL:
SELECT jsonb_build_object(
'user_id', u.id,
'name', u.name,
'is_verified', u.email IS NOT NULL,
'signup_year', EXTRACT(YEAR FROM u.created_at)
) AS user_card
FROM users u;
Здесь внутри JSON есть не только значения из колонок, но и вычисляемые поля.
is_verified считается выражением:
u.email IS NOT NULL
Если email есть, в JSON попадёт true. Если email нет, попадёт false.
signup_year берётся из даты регистрации:
EXTRACT(YEAR FROM u.created_at)
Так можно собрать аккуратный контракт ответа прямо в запросе: переименовать ключи, добавить вычисляемые поля, убрать лишние технические колонки.
Почему это лучше ручной сборки строк
Иногда новичок думает: «JSON — это же просто текст, значит, можно склеить его руками».
Например:
SELECT
'{"id": ' || id || ', "email": "' || email || '"}' AS payload
FROM users;
Такой подход быстро ломается.
Если в email попадёт кавычка, слэш или другой специальный символ, JSON может стать некорректным. Если значение окажется NULL, вся строка может стать NULL. Если число случайно взяли в кавычки, клиент получит строку вместо числа.
jsonb_build_object решает эти проблемы:
SELECT jsonb_build_object(
'id', id,
'email', email
) AS payload
FROM users;
PostgreSQL сам понимает, где строка, где число, где NULL, где булево значение. Поэтому JSON получается корректным без ручной возни с кавычками.
jsonb_build_object и повторяющиеся ключи
У jsonb_build_object результат имеет тип jsonb.
Для jsonb повторяющиеся ключи не хранятся. Если один и тот же ключ указан несколько раз, останется последнее значение.
SELECT jsonb_build_object(
'status', 'new',
'status', 'paid'
) AS payload;
Результат:
{"status": "paid"}
Это тихое поведение: PostgreSQL не выдаст предупреждение и не скажет, что ключ повторился.
Поэтому в больших объектах важно внимательно следить за именами ключей. Дубли легко не заметить, особенно когда объект собирается из многих полей.
Есть похожая функция json_build_object. Она возвращает тип json, а не jsonb. В json повторы ключей могут сохраниться в текстовом представлении. Но для большинства рабочих задач в PostgreSQL чаще выбирают именно jsonb, потому что с ним удобнее искать, сравнивать, индексировать и использовать операторы JSONB.
Важная деталь про порядок ключей
В обычном JSON порядок ключей часто выглядит как порядок, в котором вы их написали. Но для jsonb на это полагаться нельзя.
jsonb хранит объект в нормализованном виде. Он убирает лишнее форматирование, схлопывает повторяющиеся ключи и не обязан сохранять исходный порядок ключей.
Поэтому не стоит строить логику приложения на том, что ключи в JSONB-объекте выйдут ровно в том порядке, в котором вы указали их в jsonb_build_object.
Для API это обычно не проблема: JSON-объект по смыслу является набором пар key-value, а не упорядоченным списком. Если порядок важен, обычно нужен массив.
Вложенные объекты
jsonb_build_object можно вкладывать внутрь другого jsonb_build_object.
Например, соберём заказ с вложенным объектом покупателя.
SELECT jsonb_build_object(
'order_id', o.id,
'amount', o.amount,
'status', o.status,
'customer', jsonb_build_object(
'id', u.id,
'email', u.email
)
) AS order_json
FROM orders o
JOIN users u ON u.id = o.user_id;
Результат будет похож на такой:
{
"order_id": 1001,
"amount": 2500,
"status": "paid",
"customer": {
"id": 1,
"email": "ann@example.com"
}
}
Такой запрос хорошо читается: верхний объект описывает заказ, а внутри ключа customer лежит отдельный объект с данными пользователя.
Массивы внутри объекта
Если нужно добавить массив, можно использовать jsonb_build_array.
SELECT jsonb_build_object(
'order_id', o.id,
'status', o.status,
'flags', jsonb_build_array('priority', o.status)
) AS order_json
FROM orders o;
Результат:
{
"order_id": 1001,
"status": "paid",
"flags": ["priority", "paid"]
}
jsonb_build_array похож на jsonb_build_object, но собирает не объект, а массив. Ему не нужны пары key-value: он просто кладёт переданные значения по порядку.
Массив дочерних объектов через jsonb_agg
Частый сценарий: нужно получить пользователя и все его заказы внутри одного JSON.
Например:
{
"user_id": 1,
"email": "ann@example.com",
"orders": [
{"id": 1001, "amount": 2500},
{"id": 1002, "amount": 900}
]
}
Для этого jsonb_build_object удобно сочетать с агрегатом jsonb_agg.
SELECT jsonb_build_object(
'user_id', u.id,
'email', u.email,
'orders', jsonb_agg(
jsonb_build_object(
'id', o.id,
'amount', o.amount
)
ORDER BY o.created_at
)
) AS user_with_orders
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.email;
Здесь происходит несколько вещей:
- Для каждого заказа собирается маленький JSON-объект.
jsonb_agg собирает эти объекты в массив.
- Внешний
jsonb_build_object кладёт этот массив в поле orders.
ORDER BY внутри jsonb_agg очень важен. Без него порядок элементов в массиве не стоит считать надёжным. Если клиенту важно получать заказы от старых к новым, порядок нужно задать явно.
Что делать, если у пользователя нет заказов
Если использовать обычный JOIN, пользователи без заказов не попадут в результат.
Чтобы оставить всех пользователей, нужен LEFT JOIN.
SELECT jsonb_build_object(
'user_id', u.id,
'email', u.email,
'orders', COALESCE(
jsonb_agg(
jsonb_build_object(
'id', o.id,
'amount', o.amount
)
ORDER BY o.created_at
) FILTER (WHERE o.id IS NOT NULL),
'[]'::jsonb
)
) AS user_with_orders
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.email;
Здесь есть два важных приёма.
FILTER (WHERE o.id IS NOT NULL) не даёт собрать один пустой объект для пользователя без заказов.
COALESCE(..., '[]'::jsonb) превращает NULL в пустой массив.
В итоге пользователь без заказов получит:
{
"user_id": 1,
"email": "ann@example.com",
"orders": []
}
А не null и не массив с пустым заказом.
Когда использовать to_jsonb
Если вам нужно превратить всю строку таблицы в JSON, можно использовать to_jsonb.
SELECT to_jsonb(e) AS employee_json
FROM employees e;
Если в таблице employees есть колонки id, name, department, результат будет примерно таким:
{"id": 1, "name": "Ann", "department": "QA"}
to_jsonb удобен, когда нужно быстро отдать строку целиком.
Но у него есть важная особенность: ключи берутся из имён колонок. Если вы пишете to_jsonb(e), в JSON попадут все колонки строки e.
Это удобно для внутренних задач, но опасно для публичного API. Если завтра в таблицу добавят колонку salary, internal_note или admin_comment, она тоже может попасть в JSON.
Поэтому для стабильного API-контракта чаще лучше использовать jsonb_build_object и явно перечислять нужные поля.
Гибридный подход: взять строку и убрать лишнее
Иногда удобно взять строку целиком, но убрать одно или несколько полей.
Для этого у jsonb есть оператор -.
SELECT to_jsonb(e) - 'salary' AS employee_json
FROM employees e;
Так из JSON убирается ключ salary.
Можно убрать несколько ключей:
SELECT to_jsonb(e) - ARRAY['salary', 'internal_note'] AS employee_json
FROM employees e;
А можно добавить вычисляемое поле через оператор ||.
SELECT
(to_jsonb(e) - ARRAY['salary', 'internal_note'])
|| jsonb_build_object('bonus', e.salary * 0.1) AS employee_json
FROM employees e;
Здесь мы:
- Превратили строку сотрудника в
jsonb.
- Убрали чувствительные поля.
- Добавили вычисляемое поле
bonus.
Но для внешнего API всё равно будьте осторожны. Если таблица меняется, to_jsonb(e) может начать включать новые колонки. Для стабильного публичного ответа безопаснее явно описывать объект через jsonb_build_object.
Как сделать стабильный JSON через подзапрос
Есть хороший компромисс: сначала выбрать только нужные поля в подзапросе, а потом применить to_jsonb.
SELECT to_jsonb(x) AS employee_json
FROM (
SELECT
e.id,
e.name,
e.department
FROM employees e
) x;
Теперь в JSON попадут только id, name и department.
Даже если в таблицу employees позже добавят новые колонки, они не утекут в ответ, потому что подзапрос их не выбирает.
Это полезный приём, когда полей много и не хочется писать длинный jsonb_build_object, но при этом нужен контроль над тем, что уходит наружу.
Когда выбирать jsonb_build_object, а когда to_jsonb
Выбор зависит от задачи.
Если нужен точный API-контракт с понятными именами ключей, берите jsonb_build_object.
SELECT jsonb_build_object(
'user_id', u.id,
'email', u.email,
'signup_year', EXTRACT(YEAR FROM u.created_at)
) AS payload
FROM users u;
Если нужно быстро превратить всю строку в JSON для внутренней задачи, можно взять to_jsonb.
SELECT to_jsonb(u) AS user_json
FROM users u;
Если нужно взять много полей, но не все, используйте подзапрос.
SELECT to_jsonb(x) AS user_json
FROM (
SELECT
u.id,
u.email,
u.country
FROM users u
) x;
Если нужно собрать вложенную структуру, массивы, переименования и вычисляемые поля, удобнее всего jsonb_build_object вместе с jsonb_agg.
jsonb_build_object и даты
Если положить в JSON дату или timestamp, PostgreSQL сам преобразует значение в JSON-представление.
SELECT jsonb_build_object(
'id', o.id,
'created_at', o.created_at
) AS order_json
FROM orders o;
Результат может выглядеть так:
{"id": 1001, "created_at": "2024-03-15T14:30:00"}
В JSON нет отдельного типа для даты, поэтому дата обычно превращается в строку. Это нормально.
Если API требует конкретный формат даты, лучше задать его явно.
SELECT jsonb_build_object(
'id', o.id,
'created_date', to_char(o.created_at, 'YYYY-MM-DD')
) AS order_json
FROM orders o;
Так контракт становится предсказуемым: клиент всегда получает дату в нужном формате.
Практический пример: ответ с заказом
Соберём более жизненный пример: заказ, покупатель и список товаров.
Допустим, есть таблицы:
orders — заказы;
users — пользователи;
order_items — товары в заказе.
Запрос:
SELECT jsonb_build_object(
'order_id', o.id,
'status', o.status,
'amount', o.amount,
'customer', jsonb_build_object(
'id', u.id,
'email', u.email
),
'items', jsonb_agg(
jsonb_build_object(
'product_id', i.product_id,
'title', i.title,
'qty', i.qty,
'price', i.price
)
ORDER BY i.id
)
) AS order_payload
FROM orders o
JOIN users u ON u.id = o.user_id
JOIN order_items i ON i.order_id = o.id
WHERE o.id = 1001
GROUP BY o.id, o.status, o.amount, u.id, u.email;
Результат:
{
"order_id": 1001,
"status": "paid",
"amount": 2500,
"customer": {
"id": 1,
"email": "ann@example.com"
},
"items": [
{
"product_id": 501,
"title": "SQL Course",
"qty": 1,
"price": 2000
},
{
"product_id": 502,
"title": "Interview Pack",
"qty": 1,
"price": 500
}
]
}
Такой SQL уже похож на описание готового API-ответа. Видно, какие ключи уйдут наружу, где вложенный объект, где массив, какие поля вычисляются или переименовываются.
Отличие json от jsonb
В PostgreSQL есть два близких типа: json и jsonb.
Функция json_build_object возвращает json.
Функция jsonb_build_object возвращает jsonb.
На практике чаще используют jsonb, потому что он хранится в разобранном бинарном виде и поддерживает больше удобных операций: проверку ключей, удаление полей, конкатенацию, индексацию и поиск внутри структуры.
Но у jsonb есть особенности:
- порядок ключей не гарантируется;
- повторяющиеся ключи схлопываются;
- лишнее форматирование не сохраняется.
Для API-ответов это обычно нормально. Клиенту важны ключи и значения, а не пробелы и порядок ключей в объекте.
Если же вам по какой-то причине нужно сохранить текст JSON ближе к исходному виду, можно смотреть в сторону json. Но для большинства задач в PostgreSQL удобнее jsonb.
MySQL и ClickHouse
В MySQL похожая функция называется JSON_OBJECT.
SELECT JSON_OBJECT(
'id', id,
'email', email
) AS payload
FROM users;
Для массивов объектов в MySQL используют JSON_ARRAYAGG вместе с JSON_OBJECT.
SELECT JSON_OBJECT(
'user_id', u.id,
'orders', JSON_ARRAYAGG(
JSON_OBJECT(
'id', o.id,
'amount', o.amount
)
)
) AS payload
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.id;
Прямого полного аналога to_jsonb(row) из PostgreSQL в MySQL нет, поэтому ключи обычно перечисляют руками.
В ClickHouse подход зависит от версии и структуры данных. Часто JSON собирают через функции сериализации вроде toJSONString поверх кортежей, именованных кортежей или других структур.
Но идея везде одна и та же: если нужен JSON-ответ, лучше использовать функции базы для сборки JSON, а не склеивать строку руками.
Частые ошибки
Первая ошибка — нечётное количество аргументов.
SELECT jsonb_build_object(
'id', 1,
'email'
) AS payload;
У ключа 'email' нет значения, поэтому объект не соберётся.
Вторая ошибка — повторяющиеся ключи.
SELECT jsonb_build_object(
'id', 1,
'id', 2
) AS payload;
В jsonb останется последнее значение:
{"id": 2}
Третья ошибка — ожидать, что NULL удалит ключ.
SELECT jsonb_build_object(
'id', 1,
'comment', NULL
) AS payload;
Результат будет с ключом comment:
{"id": 1, "comment": null}
Если ключи со значением null нужно убрать, используйте jsonb_strip_nulls.
Четвёртая ошибка — отдавать наружу to_jsonb(table_alias) без контроля. Если в таблицу добавят новую колонку, она может неожиданно попасть в ответ.
Пятая ошибка — рассчитывать на порядок ключей в jsonb. Для объекта порядок ключей не должен иметь значения. Если порядок важен, используйте массив.
Главное из статьи
jsonb_build_object собирает JSONB-объект из пар key, value.
Аргументов должно быть чётное число: каждому ключу нужно значение.
Значения сохраняют типы: числа остаются числами, булевы значения остаются булевыми, SQL NULL превращается в JSON null.
NULL в значении не удаляет ключ. Чтобы убрать поля со значением null, используйте jsonb_strip_nulls.
NULL на месте ключа недопустим.
Если ключ повторяется несколько раз, в jsonb останется последнее значение.
jsonb_build_object удобно использовать для API-ответов, вложенных объектов и точного контроля над контрактом.
Для массивов дочерних объектов используйте связку jsonb_build_object и jsonb_agg.
Если нужно быстро превратить строку в JSON, есть to_jsonb, но для публичного API безопаснее явно перечислять поля или использовать подзапрос с нужными колонками.
Главное правило простое: не собирайте JSON руками через строки. Пусть PostgreSQL сам правильно сохранит типы, поставит кавычки, обработает NULL и соберёт корректную структуру.
jsonb_build_object— это функция PostgreSQL, которая собирает JSON-объект прямо внутри запроса.Она принимает аргументы парами:
На нечётных позициях стоят ключи, на чётных — значения.
Например:
SELECT jsonb_build_object( 'id', 1, 'email', 'ann@example.com', 'is_active', true ) AS payload;Результат:
{"id": 1, "email": "ann@example.com", "is_active": true}Эта функция особенно полезна, когда нужно отдать из базы готовый JSON для API, отчёта, webhook-события или вложенной структуры. Вместо того чтобы собирать JSON строками в приложении, можно описать нужный ответ прямо в
SELECT.Главное преимущество: PostgreSQL сам правильно сохраняет типы, расставляет кавычки, экранирует строки и превращает SQL-значения в корректный JSON.
Зачем нужен
jsonb_build_objectПредставим, что у нас есть таблица пользователей.
Мы хотим получить не обычные колонки, а JSON-объект для каждой строки.
SELECT jsonb_build_object( 'id', id, 'email', email, 'country', country, 'is_active', is_active ) AS user_json FROM users;Результат будет примерно таким:
{"id": 1, "email": "ann@example.com", "country": "DE", "is_active": true} {"id": 2, "email": "bob@example.com", "country": "BR", "is_active": false}Обратите внимание:
idостался числом,is_activeостался булевым значением, а строки получили кавычки.Если бы мы собирали JSON руками через конкатенацию строк, пришлось бы самостоятельно следить за кавычками, экранированием и типами. Это неудобно и опасно.
Плохой стиль выглядит примерно так:
SELECT '{"id": "' || id || '", "email": "' || email || '"}' AS payload FROM users;На первый взгляд работает. Но здесь
idуже стал строкой, потому что попал в кавычки. Если вemailокажется кавычка или другой специальный символ, JSON может сломаться. А если значение будетNULL, результат тоже может стать неожиданным.С
jsonb_build_objectPostgreSQL делает эту работу за вас.Базовый синтаксис
Форма функции простая:
jsonb_build_object(key, value, key, value, ...)Пример:
SELECT jsonb_build_object( 'user_id', u.id, 'email', u.email, 'country', u.country ) AS payload FROM users u;Здесь:
'user_id','email','country'— ключи будущего JSON-объекта;u.id,u.email,u.country— значения;jsonb.Ключи PostgreSQL приводит к тексту. Значения могут быть почти любого типа: числа, строки, даты, булевы значения,
NULL, массивы, другие JSON-объекты.Аргументов должно быть чётное число
У
jsonb_build_objectкаждый ключ должен иметь значение. Поэтому число аргументов обязательно должно быть чётным.Так правильно:
SELECT jsonb_build_object( 'id', 1, 'name', 'Ann' ) AS payload;А так неправильно:
SELECT jsonb_build_object( 'id', 1, 'name' ) AS payload;Во втором запросе для ключа
'name'нет значения. PostgreSQL выдаст ошибку.Логика простая: функция читает аргументы парами. Если пара не закончилась, объект собрать невозможно.
Значения сохраняют типы
Одна из главных причин использовать
jsonb_build_object— сохранение типов.SELECT jsonb_build_object( 'id', 10, 'amount', 1499.50, 'is_paid', true, 'comment', NULL ) AS payload;Результат:
{"id": 10, "amount": 1499.50, "is_paid": true, "comment": null}Здесь важно:
10— число, а не строка"10";1499.50— число, а не строка"1499.50";true— JSON-булево значение, а не строка"true";NULLпревратилось в JSONnull.Это удобно для фронтенда и внешних API. Клиенту не нужно гадать, почему цена пришла строкой, а флаг активности пришёл текстом.
Что происходит с
NULLЕсли
NULLстоит на месте значения, ключ остаётся в объекте, а значение становится JSONnull.SELECT jsonb_build_object( 'id', 1, 'middle_name', NULL ) AS payload;Результат:
{"id": 1, "middle_name": null}Это нормальное поведение. Ключ не исчезает.
Если вы хотите убрать поля со значением
null, можно дополнительно использоватьjsonb_strip_nulls.SELECT jsonb_strip_nulls( jsonb_build_object( 'id', 1, 'middle_name', NULL ) ) AS payload;Результат:
{"id": 1}А вот
NULLна месте ключа использовать нельзя.SELECT jsonb_build_object( NULL, 'Ann' ) AS payload;Ключ JSON-объекта должен существовать. Поэтому такой запрос завершится ошибкой.
Пример: карточка пользователя для API
Самый частый сценарий — собрать ответ для API прямо в запросе.
Допустим, нам нужна карточка пользователя:
{ "user_id": 1, "name": "Ann", "is_verified": true, "signup_year": 2024 }Соберём её в SQL:
SELECT jsonb_build_object( 'user_id', u.id, 'name', u.name, 'is_verified', u.email IS NOT NULL, 'signup_year', EXTRACT(YEAR FROM u.created_at) ) AS user_card FROM users u;Здесь внутри JSON есть не только значения из колонок, но и вычисляемые поля.
is_verifiedсчитается выражением:u.email IS NOT NULLЕсли email есть, в JSON попадёт
true. Если email нет, попадётfalse.signup_yearберётся из даты регистрации:EXTRACT(YEAR FROM u.created_at)Так можно собрать аккуратный контракт ответа прямо в запросе: переименовать ключи, добавить вычисляемые поля, убрать лишние технические колонки.
Почему это лучше ручной сборки строк
Иногда новичок думает: «JSON — это же просто текст, значит, можно склеить его руками».
Например:
SELECT '{"id": ' || id || ', "email": "' || email || '"}' AS payload FROM users;Такой подход быстро ломается.
Если в email попадёт кавычка, слэш или другой специальный символ, JSON может стать некорректным. Если значение окажется
NULL, вся строка может статьNULL. Если число случайно взяли в кавычки, клиент получит строку вместо числа.jsonb_build_objectрешает эти проблемы:SELECT jsonb_build_object( 'id', id, 'email', email ) AS payload FROM users;PostgreSQL сам понимает, где строка, где число, где
NULL, где булево значение. Поэтому JSON получается корректным без ручной возни с кавычками.jsonb_build_objectи повторяющиеся ключиУ
jsonb_build_objectрезультат имеет типjsonb.Для
jsonbповторяющиеся ключи не хранятся. Если один и тот же ключ указан несколько раз, останется последнее значение.SELECT jsonb_build_object( 'status', 'new', 'status', 'paid' ) AS payload;Результат:
{"status": "paid"}Это тихое поведение: PostgreSQL не выдаст предупреждение и не скажет, что ключ повторился.
Поэтому в больших объектах важно внимательно следить за именами ключей. Дубли легко не заметить, особенно когда объект собирается из многих полей.
Есть похожая функция
json_build_object. Она возвращает типjson, а неjsonb. Вjsonповторы ключей могут сохраниться в текстовом представлении. Но для большинства рабочих задач в PostgreSQL чаще выбирают именноjsonb, потому что с ним удобнее искать, сравнивать, индексировать и использовать операторы JSONB.Важная деталь про порядок ключей
В обычном JSON порядок ключей часто выглядит как порядок, в котором вы их написали. Но для
jsonbна это полагаться нельзя.jsonbхранит объект в нормализованном виде. Он убирает лишнее форматирование, схлопывает повторяющиеся ключи и не обязан сохранять исходный порядок ключей.Поэтому не стоит строить логику приложения на том, что ключи в JSONB-объекте выйдут ровно в том порядке, в котором вы указали их в
jsonb_build_object.Для API это обычно не проблема: JSON-объект по смыслу является набором пар
key-value, а не упорядоченным списком. Если порядок важен, обычно нужен массив.Вложенные объекты
jsonb_build_objectможно вкладывать внутрь другогоjsonb_build_object.Например, соберём заказ с вложенным объектом покупателя.
SELECT jsonb_build_object( 'order_id', o.id, 'amount', o.amount, 'status', o.status, 'customer', jsonb_build_object( 'id', u.id, 'email', u.email ) ) AS order_json FROM orders o JOIN users u ON u.id = o.user_id;Результат будет похож на такой:
{ "order_id": 1001, "amount": 2500, "status": "paid", "customer": { "id": 1, "email": "ann@example.com" } }Такой запрос хорошо читается: верхний объект описывает заказ, а внутри ключа
customerлежит отдельный объект с данными пользователя.Массивы внутри объекта
Если нужно добавить массив, можно использовать
jsonb_build_array.SELECT jsonb_build_object( 'order_id', o.id, 'status', o.status, 'flags', jsonb_build_array('priority', o.status) ) AS order_json FROM orders o;Результат:
{ "order_id": 1001, "status": "paid", "flags": ["priority", "paid"] }jsonb_build_arrayпохож наjsonb_build_object, но собирает не объект, а массив. Ему не нужны парыkey-value: он просто кладёт переданные значения по порядку.Массив дочерних объектов через
jsonb_aggЧастый сценарий: нужно получить пользователя и все его заказы внутри одного JSON.
Например:
{ "user_id": 1, "email": "ann@example.com", "orders": [ {"id": 1001, "amount": 2500}, {"id": 1002, "amount": 900} ] }Для этого
jsonb_build_objectудобно сочетать с агрегатомjsonb_agg.SELECT jsonb_build_object( 'user_id', u.id, 'email', u.email, 'orders', jsonb_agg( jsonb_build_object( 'id', o.id, 'amount', o.amount ) ORDER BY o.created_at ) ) AS user_with_orders FROM users u JOIN orders o ON o.user_id = u.id GROUP BY u.id, u.email;Здесь происходит несколько вещей:
jsonb_aggсобирает эти объекты в массив.jsonb_build_objectкладёт этот массив в полеorders.ORDER BYвнутриjsonb_aggочень важен. Без него порядок элементов в массиве не стоит считать надёжным. Если клиенту важно получать заказы от старых к новым, порядок нужно задать явно.Что делать, если у пользователя нет заказов
Если использовать обычный
JOIN, пользователи без заказов не попадут в результат.Чтобы оставить всех пользователей, нужен
LEFT JOIN.SELECT jsonb_build_object( 'user_id', u.id, 'email', u.email, 'orders', COALESCE( jsonb_agg( jsonb_build_object( 'id', o.id, 'amount', o.amount ) ORDER BY o.created_at ) FILTER (WHERE o.id IS NOT NULL), '[]'::jsonb ) ) AS user_with_orders FROM users u LEFT JOIN orders o ON o.user_id = u.id GROUP BY u.id, u.email;Здесь есть два важных приёма.
FILTER (WHERE o.id IS NOT NULL)не даёт собрать один пустой объект для пользователя без заказов.COALESCE(..., '[]'::jsonb)превращаетNULLв пустой массив.В итоге пользователь без заказов получит:
{ "user_id": 1, "email": "ann@example.com", "orders": [] }А не
nullи не массив с пустым заказом.Когда использовать
to_jsonbЕсли вам нужно превратить всю строку таблицы в JSON, можно использовать
to_jsonb.SELECT to_jsonb(e) AS employee_json FROM employees e;Если в таблице
employeesесть колонкиid,name,department, результат будет примерно таким:{"id": 1, "name": "Ann", "department": "QA"}to_jsonbудобен, когда нужно быстро отдать строку целиком.Но у него есть важная особенность: ключи берутся из имён колонок. Если вы пишете
to_jsonb(e), в JSON попадут все колонки строкиe.Это удобно для внутренних задач, но опасно для публичного API. Если завтра в таблицу добавят колонку
salary,internal_noteилиadmin_comment, она тоже может попасть в JSON.Поэтому для стабильного API-контракта чаще лучше использовать
jsonb_build_objectи явно перечислять нужные поля.Гибридный подход: взять строку и убрать лишнее
Иногда удобно взять строку целиком, но убрать одно или несколько полей.
Для этого у
jsonbесть оператор-.SELECT to_jsonb(e) - 'salary' AS employee_json FROM employees e;Так из JSON убирается ключ
salary.Можно убрать несколько ключей:
SELECT to_jsonb(e) - ARRAY['salary', 'internal_note'] AS employee_json FROM employees e;А можно добавить вычисляемое поле через оператор
||.SELECT (to_jsonb(e) - ARRAY['salary', 'internal_note']) || jsonb_build_object('bonus', e.salary * 0.1) AS employee_json FROM employees e;Здесь мы:
jsonb.bonus.Но для внешнего API всё равно будьте осторожны. Если таблица меняется,
to_jsonb(e)может начать включать новые колонки. Для стабильного публичного ответа безопаснее явно описывать объект черезjsonb_build_object.Как сделать стабильный JSON через подзапрос
Есть хороший компромисс: сначала выбрать только нужные поля в подзапросе, а потом применить
to_jsonb.SELECT to_jsonb(x) AS employee_json FROM ( SELECT e.id, e.name, e.department FROM employees e ) x;Теперь в JSON попадут только
id,nameиdepartment.Даже если в таблицу
employeesпозже добавят новые колонки, они не утекут в ответ, потому что подзапрос их не выбирает.Это полезный приём, когда полей много и не хочется писать длинный
jsonb_build_object, но при этом нужен контроль над тем, что уходит наружу.Когда выбирать
jsonb_build_object, а когдаto_jsonbВыбор зависит от задачи.
Если нужен точный API-контракт с понятными именами ключей, берите
jsonb_build_object.SELECT jsonb_build_object( 'user_id', u.id, 'email', u.email, 'signup_year', EXTRACT(YEAR FROM u.created_at) ) AS payload FROM users u;Если нужно быстро превратить всю строку в JSON для внутренней задачи, можно взять
to_jsonb.SELECT to_jsonb(u) AS user_json FROM users u;Если нужно взять много полей, но не все, используйте подзапрос.
SELECT to_jsonb(x) AS user_json FROM ( SELECT u.id, u.email, u.country FROM users u ) x;Если нужно собрать вложенную структуру, массивы, переименования и вычисляемые поля, удобнее всего
jsonb_build_objectвместе сjsonb_agg.jsonb_build_objectи датыЕсли положить в JSON дату или timestamp, PostgreSQL сам преобразует значение в JSON-представление.
SELECT jsonb_build_object( 'id', o.id, 'created_at', o.created_at ) AS order_json FROM orders o;Результат может выглядеть так:
{"id": 1001, "created_at": "2024-03-15T14:30:00"}В JSON нет отдельного типа для даты, поэтому дата обычно превращается в строку. Это нормально.
Если API требует конкретный формат даты, лучше задать его явно.
SELECT jsonb_build_object( 'id', o.id, 'created_date', to_char(o.created_at, 'YYYY-MM-DD') ) AS order_json FROM orders o;Так контракт становится предсказуемым: клиент всегда получает дату в нужном формате.
Практический пример: ответ с заказом
Соберём более жизненный пример: заказ, покупатель и список товаров.
Допустим, есть таблицы:
orders— заказы;users— пользователи;order_items— товары в заказе.Запрос:
SELECT jsonb_build_object( 'order_id', o.id, 'status', o.status, 'amount', o.amount, 'customer', jsonb_build_object( 'id', u.id, 'email', u.email ), 'items', jsonb_agg( jsonb_build_object( 'product_id', i.product_id, 'title', i.title, 'qty', i.qty, 'price', i.price ) ORDER BY i.id ) ) AS order_payload FROM orders o JOIN users u ON u.id = o.user_id JOIN order_items i ON i.order_id = o.id WHERE o.id = 1001 GROUP BY o.id, o.status, o.amount, u.id, u.email;Результат:
{ "order_id": 1001, "status": "paid", "amount": 2500, "customer": { "id": 1, "email": "ann@example.com" }, "items": [ { "product_id": 501, "title": "SQL Course", "qty": 1, "price": 2000 }, { "product_id": 502, "title": "Interview Pack", "qty": 1, "price": 500 } ] }Такой SQL уже похож на описание готового API-ответа. Видно, какие ключи уйдут наружу, где вложенный объект, где массив, какие поля вычисляются или переименовываются.
Отличие
jsonотjsonbВ PostgreSQL есть два близких типа:
jsonиjsonb.Функция
json_build_objectвозвращаетjson.Функция
jsonb_build_objectвозвращаетjsonb.На практике чаще используют
jsonb, потому что он хранится в разобранном бинарном виде и поддерживает больше удобных операций: проверку ключей, удаление полей, конкатенацию, индексацию и поиск внутри структуры.Но у
jsonbесть особенности:Для API-ответов это обычно нормально. Клиенту важны ключи и значения, а не пробелы и порядок ключей в объекте.
Если же вам по какой-то причине нужно сохранить текст JSON ближе к исходному виду, можно смотреть в сторону
json. Но для большинства задач в PostgreSQL удобнееjsonb.MySQL и ClickHouse
В MySQL похожая функция называется
JSON_OBJECT.SELECT JSON_OBJECT( 'id', id, 'email', email ) AS payload FROM users;Для массивов объектов в MySQL используют
JSON_ARRAYAGGвместе сJSON_OBJECT.SELECT JSON_OBJECT( 'user_id', u.id, 'orders', JSON_ARRAYAGG( JSON_OBJECT( 'id', o.id, 'amount', o.amount ) ) ) AS payload FROM users u JOIN orders o ON o.user_id = u.id GROUP BY u.id;Прямого полного аналога
to_jsonb(row)из PostgreSQL в MySQL нет, поэтому ключи обычно перечисляют руками.В ClickHouse подход зависит от версии и структуры данных. Часто JSON собирают через функции сериализации вроде
toJSONStringповерх кортежей, именованных кортежей или других структур.Но идея везде одна и та же: если нужен JSON-ответ, лучше использовать функции базы для сборки JSON, а не склеивать строку руками.
Частые ошибки
Первая ошибка — нечётное количество аргументов.
SELECT jsonb_build_object( 'id', 1, 'email' ) AS payload;У ключа
'email'нет значения, поэтому объект не соберётся.Вторая ошибка — повторяющиеся ключи.
SELECT jsonb_build_object( 'id', 1, 'id', 2 ) AS payload;В
jsonbостанется последнее значение:{"id": 2}Третья ошибка — ожидать, что
NULLудалит ключ.SELECT jsonb_build_object( 'id', 1, 'comment', NULL ) AS payload;Результат будет с ключом
comment:{"id": 1, "comment": null}Если ключи со значением
nullнужно убрать, используйтеjsonb_strip_nulls.Четвёртая ошибка — отдавать наружу
to_jsonb(table_alias)без контроля. Если в таблицу добавят новую колонку, она может неожиданно попасть в ответ.Пятая ошибка — рассчитывать на порядок ключей в
jsonb. Для объекта порядок ключей не должен иметь значения. Если порядок важен, используйте массив.Главное из статьи
jsonb_build_objectсобирает JSONB-объект из парkey, value.Аргументов должно быть чётное число: каждому ключу нужно значение.
Значения сохраняют типы: числа остаются числами, булевы значения остаются булевыми, SQL
NULLпревращается в JSONnull.NULLв значении не удаляет ключ. Чтобы убрать поля со значениемnull, используйтеjsonb_strip_nulls.NULLна месте ключа недопустим.Если ключ повторяется несколько раз, в
jsonbостанется последнее значение.jsonb_build_objectудобно использовать для API-ответов, вложенных объектов и точного контроля над контрактом.Для массивов дочерних объектов используйте связку
jsonb_build_objectиjsonb_agg.Если нужно быстро превратить строку в JSON, есть
to_jsonb, но для публичного API безопаснее явно перечислять поля или использовать подзапрос с нужными колонками.Главное правило простое: не собирайте JSON руками через строки. Пусть PostgreSQL сам правильно сохранит типы, поставит кавычки, обработает
NULLи соберёт корректную структуру.