To deal with the problem of infinite recursion, you need to use a recursive CTE to work with each individual json element in each row of the table:
WITH RECURSIVE raw_json as ( SELECT * FROM (VALUES (1, '{ "customerId": "12345", "orders": [ { "orderId": "54321", "lineItems": [ { "productId": "abc", "qty": 3 }, { "productId": "def", "qty": 1 } ] } ] }'::json), (2, '{ "customerId": "678910", "artibitraryLevel": { "orders": [ { "orderId": "55345", "lineItems": [ { "productId": "abc", "qty": 3 }, { "productId": "ghi", "qty": 10 } ] } ] } }'::json) ) a(id,sample_json) ), json_recursive as ( SELECT a.id, bk, bv, b.json_type, case when b.json_type = 'object' and not (bv->>'customerId') is null then bv->>'customerId' else a.customer_id end customer_id, --track any arbitrary id when iterating through json graph case when b.json_type = 'object' and not (bv->>'orderId') is null then bv->>'orderId' else a.order_id end order_id, case when b.json_type = 'object' and not (bv->>'productId') is null then bv->>'productId' else a.product_id end product_id FROM ( SELECT id, sample_json v, case left(sample_json::text,1) when '[' then 'array' when '{' then 'object' else 'scalar' end json_type, --because choice of json accessor function depends on this, and for some reason postgres has no built in function to get this value sample_json->>'customerId' customer_id, sample_json->>'orderId' order_id, sample_json->>'productId' product_id FROM raw_json ) a CROSS JOIN LATERAL ( SELECT bk, bv, case left(bv::text,1) when '[' then 'array' when '{' then 'object' else 'scalar' end json_type FROM json_each(case json_type when 'object' then av else null end ) b(k,v) --get key value pairs for individual elements if we are dealing with standard object UNION ALL SELECT null::text k, cv, case left(cv::text,1) when '[' then 'array' when '{' then 'object' else 'scalar' end json_type FROM json_array_elements(case json_type when 'array' then av else null end) c(v) --if we have an array, just get the elements and use parent key ) b UNION ALL --recursive term SELECT a.id, bk, bv, b.json_type, case when b.json_type = 'object' and not (bv->>'customerId') is null then bv->>'customerId' else a.customer_id end customer_id, case when b.json_type = 'object' and not (bv->>'orderId') is null then bv->>'orderId' else a.order_id end order_id, case when b.json_type = 'object' and not (bv->>'productId') is null then bv->>'productId' else a.product_id end product_id FROM json_recursive a CROSS JOIN LATERAL ( SELECT bk, bv, case left(bv::text,1) when '[' then 'array' when '{' then 'object' else 'scalar' end json_type FROM json_each(case json_type when 'object' then av else null end ) b(k,v) UNION ALL SELECT ak, cv, case left(cv::text,1) when '[' then 'array' when '{' then 'object' else 'scalar' end json_type FROM json_array_elements(case json_type when 'array' then av else null end) c(v) ) b )
Then you can either summarize "qty" with an arbitrary id ...
SELECT customer_id, sum(v::text::integer) FROM json_recursive WHERE k = 'qty' GROUP BY customer_id
Or you can get the "lineItem" objects and manage them as you wish:
SELECT * FROM json_recursive WHERE k = 'lineItems' and json_type = 'object'
Regarding indexing, you can adapt a recursive query to a function that returns unique keys for each json object in each row of the source table, and then to create a functional index in your json column:
SELECT array_agg(DISTINCT k) FROM json_recursive WHERE not k is null