Skip to main content

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​

QuestionAnswer
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 typeRight typeWhich || runsResult
texttextstring concat'[1,2]' || '[3]' → '[1,2][3]' (glued text)
jsonbjsonbJSONB 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​

ToolUse it when
array || arrayAppending a new item to a JSONB array (e.g. a history log)
object || objectAdding/replacing whole top-level keys in a flat object
jsonb_setChanging one nested field without disturbing its siblings
Custom function / app codeYou 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 jsonb is 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_set edits 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.