Freaking difficult, took me hours to solve :-P
CREATE TABLE foo (
id INT PRIMARY KEY,
parent_id INT);
insert into foo values (1, null);
insert into foo values (2, 1);
insert into foo values (3, 2);
WITH RECURSIVE
tree AS (
SELECT 1 AS round, id, parent_id, ARRAY(SELECT id FROM foo WHERE parent_id = f.id) AS children
FROM foo f
WHERE id = 1
UNION ALL
SELECT round+1, f.id, f.parent_id, ARRAY(SELECT id FROM foo WHERE parent_id = f.id) AS children
FROM tree t
JOIN foo f ON (f.id = ANY(t.children))
),
rev AS (
SELECT r.round-1 AS round,
to_jsonb(ARRAY(
SELECT a
FROM (
SELECT f.parent_id AS id, json_agg(jsonb_build_object('id', f.id, 'children', '{}'::text[])) AS children
FROM tree t
JOIN foo f ON (f.id = t.id)
WHERE t.round = r.round
GROUP BY f.parent_id
) a
)) AS list
FROM (SELECT MAX(round)::int AS round FROM tree) r
UNION ALL
SELECT r.round-1,
to_jsonb(ARRAY(
SELECT a
FROM (
SELECT f.parent_id AS id, json_agg(jsonb_build_object('id', f.id, 'children', t->'children')) AS children
FROM jsonb_array_elements(list) t
JOIN foo f ON (f.id = (t->>'id')::int)
GROUP BY f.parent_id
) a
)) AS list
FROM rev r
WHERE round > 1
)
SELECT list as nested_json_tree
FROM rev
WHERE round = 1
The complexity lies in the requirement to build first the tree (top down), and then build the object from the tree, bottom-up. Recursive bottom-up is tricky due to limitation in recursive queries, such as the recursive alias within the UNION ALL section not being able to being grouped, nor included in a subquery. I solved this by doing the unwrapping via reversed rounds.
This query should properly build complex trees, with multiple children per node, and any number of nesting levels.