sqlpostgresqljsonbjson

`to_jsonb` в PostgreSQL: как превратить строку таблицы в JSON

Как to_jsonb превращает любое значение, строку или массив в jsonb, сохраняя типы, и чем он умнее json_build_object и старого row_to_json.

9 мин чтенияСправочникsql · postgresql · jsonb · json · row_to_json

Когда приложение просит у базы данные, ему часто нужен не набор отдельных колонок, а готовый 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.

Например, есть пользователь без имени:

id email name
10 user@example.com 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>"
}

Здесь:

  1. to_jsonb(u) делает объект из строки пользователя.
  2. jsonb_build_object(...) создаёт дополнительный объект.
  3. || склеивает два объекта в один.

Если в обоих объектах есть одинаковый ключ, значение справа перезапишет значение слева.

Например:

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"
    }
  ]
}

Разберём по частям.

Внешняя часть:

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"
    }
  ]
}

В одном запросе мы:

  1. Превратили пользователя в JSON.
  2. Убрали закрытые поля.
  3. Добавили количество заказов.
  4. Добавили массив заказов.
  5. Убрали из заказов лишний user_id.
  6. Подстраховались пустым массивом через 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». Без ручной склейки, без охоты за кавычками и без потери типов.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр