-> vs ->> vs #> vs #>> operators. — Cracked Java
// PostgreSQL · JSON & JSONB
MidCodingTrick

-> vs ->> vs #> vs #>> operators.

The four access operators split along two axes: single-key vs path, and JSON-result vs text-result. Getting -> vs ->> right is the single most common JSON mistake — the wrong one breaks comparisons and casts.

-> returns json/jsonb; ->> returns text

Both take a single key (or array index). The arrow with two characters (->>) "exits" to text; the single arrow stays in JSON.

SELECT '{"city": "Paris"}'::jsonb -> 'city';    -- "Paris"   (jsonb, note the quotes)
SELECT '{"city": "Paris"}'::jsonb ->> 'city';   -- Paris     (text, no quotes)

SELECT '[10, 20, 30]'::jsonb -> 1;              -- 20        (jsonb)
SELECT '[10, 20, 30]'::jsonb ->> 1;             -- 20        (text)

The distinction matters the moment you compare or cast:

WHERE data ->> 'age' = '30'                     -- text compare, works
WHERE (data ->> 'age')::int > 30                -- cast text to int, works
WHERE data -> 'age' = '30'                      -- comparing jsonb to text: error / mismatch

A jsonb value can be compared to another jsonb (data -> 'age' = '30'::jsonb), but to get a normal SQL value out — a string, a number — you almost always want ->>.

#> and #>> follow a path

These take a text[] path array and descend through nested levels in one step. #> returns jsonb; #>> returns text — same JSON-vs-text rule.

SELECT data #>  '{address,city}'    FROM users;   -- jsonb
SELECT data #>> '{address,city}'    FROM users;   -- text

-- arrays use numeric indices as path elements:
SELECT data #>> '{tags,0}'          FROM posts;   -- first tag as text

data #> '{address,city}' is equivalent to data -> 'address' -> 'city', and data #>> '{address,city}' equals data -> 'address' ->> 'city'. Chaining single arrows is fine for shallow access; the path form reads better when the path is deep or built dynamically.

Null behavior

If any step doesn't exist, the operators return SQL NULL rather than erroring — so missing keys silently produce NULL, which then participates in WHERE like any other null.

Mark your status