PostgreSQL JSONB Merge Tools
Summary: PostgreSQL has three separate tools for combining JSONB values —
\|\| for arrays, \|\| for objects, and jsonb_set for one nested field — and no
built-in tool for a true recursive deep merge.
At a glance
| Question | Answer |
|---|---|
| What is it? | Ways to combine or update JSONB values in PostgreSQL |
| Why does it matter? | || behaves differently depending on whether both sides are arrays or objects, and can silently overwrite nested data |
| When do I use it? | Any UPDATE that appends to a JSONB array, merges JSONB objects, or edits a nested JSONB field |
Casting: why ::jsonb comes first
'[1,2]' is plain text until it's cast. ::jsonb parses that text as JSON and
converts it to the jsonb type — this is what makes Postgres recognize it as
an array (or object) at all, not an extra step on top of an already-recognized
array.
\|\| is an overloaded operator: Postgres picks a different version of it based
on the types on both sides.
| Left type | Right type | Which || runs | Result |
|---|---|---|---|
text | text | string concat | '[1,2]' || '[3]' → '[1,2][3]' (glued text) |
jsonb | jsonb | JSONB concat | '[1,2]'::jsonb || '[3]'::jsonb → [1, 2, 3] (merged array) |
A cast is unnecessary when a value is already typed jsonb — e.g. a jsonb
column, or a driver parameter already bound as jsonb. It's required for raw
text literals and string parameters (like %s from Python).
How \|\| and jsonb_set behave
array \|\| array→ concatenates elements into one array.object \|\| object→ merges keys at the top level only. A key present on both sides is fully replaced by the right side's value — even if both are nested objects.\|\|never descends into a nested object to merge its fields; it swaps the whole value.jsonb_set(target, path, value)→ updates one exact path, leaves everything else untouched.- No built-in deep merge — recursively merging nested objects (so nested keys combine instead of one replacing the other) needs a custom function or app-side logic.
When to use each
| Tool | Use it when |
|---|---|
array || array | Appending a new item to a JSONB array (e.g. a history log) |
object || object | Adding/replacing whole top-level keys in a flat object |
jsonb_set | Changing one nested field without disturbing its siblings |
| Custom function / app code | You need to merge nested objects and keep both sides' unrelated keys |
Example
select '[1,2]'::jsonb || '[3]'::jsonb; -- [1, 2, 3]
select '{"a":1}'::jsonb || '{"b":2}'::jsonb; -- {"a": 1, "b": 2}
-- Danger case: object || object silently drops sibling keys
select '{"round":1}'::jsonb || '{"round":2}'::jsonb;
-- {"round": 2} <-- "round":1 is gone, this is object-merge, not append
-- Safe version: keep both sides as arrays so it's always array-concat
select '[{"round":1}]'::jsonb || '[{"round":2}]'::jsonb;
-- [{"round": 1}, {"round": 2}]
select jsonb_set('{"user":{"name":"A","age":30}}'::jsonb,
'{user,age}', '31'::jsonb);
-- {"user": {"name": "A", "age": 31}}
Remember
- Casting to
jsonbis what makes Postgres treat a value as an array/object at all — it determines which\|\|overload runs, not just which tools unlock. \|\|is shallow: a shared key's value is replaced wholesale, never merged.- Wrap a single object in
[ ]before\|\|-ing it onto an array to guarantee array-concat behavior, especially if the column's shape could ever be wrong. jsonb_setedits one path per call; chain calls (feeding one's output into the next) to update several nested fields.- Deep merge has no built-in — write a recursive function, or merge the two objects in application code before sending the final value to Postgres.
Related notes
- JSON Vs JSONB Storage - why these operators only exist for
jsonb, notjson