Skip to content

JSON / JSONB

UQL provides first-class support for JSON/JSONB fields across PostgreSQL, MySQL, MariaDB, and SQLite. Query, update, and sort by nested JSON properties using a consistent, type-safe API. UQL generates dialect-specific SQL automatically.

Wrap JSONB field types with Json<T> to enable full type safety: IDE autocompletion for dot-notation paths, $merge keys, $unset keys, and $push targets.

import { Entity, Id, Field, Json } from 'uql-orm';
@Entity()
export class Company {
@Id()
id?: number;
@Field()
name?: string;
@Field({ type: 'jsonb' })
kind?: Json<{ public?: number; private?: number; tags?: string[] }>;
@Field({ type: 'jsonb' })
settings?: Json<{ isArchived?: boolean; theme?: string; locale?: string }>;
}

Query nested JSON properties using dot-notation paths in $where. All comparison operators are supported.

You write
const companies = await querier.findMany(Company, {
$where: {
'settings.isArchived': { $ne: true },
'settings.theme': 'dark',
},
});
Generated SQL (PostgreSQL, node-pg)
SELECT * FROM "Company"
WHERE ("settings"->>'isArchived') IS DISTINCT FROM $1
AND "settings"->>'theme' = $2
Generated SQL (MySQL)
SELECT * FROM `Company`
WHERE (`settings`->>'isArchived') <> ?
AND (`settings`->>'theme') = ?
Generated SQL (MariaDB)
SELECT * FROM `Company`
WHERE JSON_VALUE(`settings`, '$.isArchived') <> ?
AND JSON_VALUE(`settings`, '$.theme') = ?
Generated SQL (SQLite)
SELECT * FROM `Company`
WHERE json_extract(`settings`, '$.isArchived') IS NOT ?
AND json_extract(`settings`, '$.theme') = ?

Atomically merge or remove keys in JSON fields directly from update payloads. No need to overwrite the entire JSON value.

Merge new key-value pairs into an existing JSON field. Existing keys not in $merge are preserved.

You write
await querier.updateOneById(Company, id, {
kind: { $merge: { public: 1 } },
});
Generated SQL (PostgreSQL, node-pg)
UPDATE "Company" SET "kind" = COALESCE("kind", '{}'::jsonb) || $1::jsonb WHERE "id" = $2
-- values: ['{"public":1}', id]
Generated SQL (MySQL)
UPDATE `Company` SET `kind` = JSON_SET(COALESCE(`kind`, '{}'), '$.public', CAST(? AS JSON)) WHERE `id` = ?
-- values: ['1', id]
Generated SQL (MariaDB)
UPDATE `Company` SET `kind` = JSON_SET(COALESCE(`kind`, '{}'), '$.public', CAST(? AS JSON)) WHERE `id` = ?
-- values: ['1', id]
Generated SQL (SQLite)
UPDATE `Company` SET `kind` = json_set(COALESCE(`kind`, '{}'), '$.public', json(?)) WHERE `id` = ?
-- values: ['1', id]

Remove specific keys from a JSON field.

You write
await querier.updateOneById(Company, id, {
kind: { $unset: ['private'] },
});
Generated SQL (PostgreSQL, node-pg)
UPDATE "Company" SET "kind" = ("kind") - 'private' WHERE "id" = $1
Generated SQL (MySQL)
UPDATE `Company` SET `kind` = JSON_REMOVE(`kind`, '$.private') WHERE `id` = ?
Generated SQL (MariaDB)
UPDATE `Company` SET `kind` = JSON_REMOVE(`kind`, '$.private') WHERE `id` = ?
Generated SQL (SQLite)
UPDATE `Company` SET `kind` = json_remove(`kind`, '$.private') WHERE `id` = ?

Append a value to the end of a JSON array using native array-append functions. Only keys whose type is an array are valid $push targets (type-checked at compile time).

You write
await querier.updateOneById(Company, id, {
kind: { $push: { tags: 'new-tag' } },
});
Generated SQL (PostgreSQL, node-pg)
UPDATE "Company" SET "kind" = jsonb_set("kind", '{tags}', COALESCE("kind"->'tags', '[]'::jsonb) || jsonb_build_array($1::jsonb)) WHERE "id" = $2
-- values: ['"new-tag"', id]
Generated SQL (MySQL)
UPDATE `Company` SET `kind` = JSON_ARRAY_APPEND(`kind`, '$.tags', CAST(? AS JSON)) WHERE `id` = ?
-- values: ['"new-tag"', id]
Generated SQL (MariaDB)
UPDATE `Company` SET `kind` = JSON_ARRAY_APPEND(`kind`, '$.tags', CAST(? AS JSON)) WHERE `id` = ?
-- values: ['"new-tag"', id]
Generated SQL (SQLite)
UPDATE `Company` SET `kind` = json_insert(`kind`, '$.tags[#]', json(?)) WHERE `id` = ?
-- values: ['"new-tag"', id]

$merge, $unset, and $push can be freely combined in a single update.

You write
await querier.updateOneById(Company, id, {
kind: { $merge: { public: 1 }, $push: { tags: 'new-tag' }, $unset: ['private'] },
});

Sort by nested JSON field values using the same dot-notation syntax.

You write
const companies = await querier.findMany(Company, {
$sort: { 'kind.public': 'desc' },
});
Generated SQL (PostgreSQL, node-pg)
SELECT * FROM "Company" ORDER BY "kind"->>'public' DESC
Generated SQL (MySQL)
SELECT * FROM `Company` ORDER BY (`kind`->>'public') DESC
Generated SQL (MariaDB)
SELECT * FROM `Company` ORDER BY JSON_VALUE(`kind`, '$.public') DESC
Generated SQL (SQLite)
SELECT * FROM `Company` ORDER BY json_extract(`kind`, '$.public') DESC

All JSON features work across four SQL dialects:

Feature PostgreSQL MySQL MariaDB SQLite
Dot-notation filtering ->>'key' ->>'key' JSON_VALUE() json_extract()
$merge || ::jsonb JSON_SET() JSON_SET() json_set()
$unset - 'key' JSON_REMOVE() JSON_REMOVE() json_remove()
$push jsonb_set() + || JSON_ARRAY_APPEND() JSON_ARRAY_APPEND() json_insert()
Dot-notation sorting ->>'key' ->>'key' JSON_VALUE() json_extract()
$size jsonb_array_length() JSON_LENGTH() JSON_LENGTH() json_array_length()
$all @> ::jsonb JSON_CONTAINS() JSON_CONTAINS() json_each()
$elemMatch jsonb_array_elements JSON_TABLE() JSON_TABLE() json_each()

This page targets modern, actively maintained database lines. Baselines below reflect the current compatibility target for generated SQL:

Dialect Practical baseline Notes
PostgreSQL 16+ Uses jsonb operators/functions (->>, `
MySQL 8.4+ Uses ->>, JSON_SET, JSON_REMOVE, JSON_ARRAY_APPEND, JSON_TABLE
MariaDB 12.2+ Uses JSON_VALUE for dot-notation path extraction (not ->>), plus JSON_SET, JSON_REMOVE, JSON_ARRAY_APPEND, JSON_TABLE
SQLite 3.45+ Uses json_extract, json_set, json_remove, and json_insert(..., '$[#]', ...) for append