← Back to TIL

Recursively expanding JSON fields in postgres

An example query is:

WITH RECURSIVE extract_all AS (

	SELECT
		id,
		jsonb_typeof(value) AS typ,
		key,
		key AS path,
		value
	FROM
		table_with_json_col,
		jsonb_each(json_col)
		
		
	UNION ALL
	
	
	SELECT
		id,
		jsonb_typeof(coalesce(arr_value, obj_value)) as typ,
		key || CASE
			WHEN obj_key IS NOT NULL THEN ('.' || obj_key)
			ELSE ''
		END as key,
		path || '.' || coalesce(obj_key, (arr_key - 1)::text) as path,
		coalesce(arr_value, obj_value) as value
		
	FROM extract_all
	
	LEFT JOIN LATERAL jsonb_array_elements(
		CASE 
			WHEN typ = 'array' THEN value
			ELSE NULL
		END
	)
	WITH ORDINALITY AS a (arr_value, arr_key)
	ON typ = 'array'
	
	
	LEFT JOIN LATERAL jsonb_each(
		CASE 
			WHEN typ = 'object' THEN value
			ELSE NULL
		END
	) AS o (obj_key, obj_value)
	ON typ = 'object'
	
	WHERE typ IN ('array', 'object'))
SELECT
	id,
	table_name,
	typ,
	key,
	path,
	CASE
		WHEN typ = 'string'
		THEN JSON_VALUE (value, '$')::text
		
		ELSE NULL::text
	END AS value_text,
	
	CASE
		WHEN typ = 'number'
		THEN JSON_VALUE (value, '$')::numeric
		
		ELSE NULL::numeric
	END AS value_number,
	
	CASE 
		WHEN typ = 'boolean'
		THEN JSON_VALUE (value, '$')::bool
		
		ELSE NULL::bool
	END AS value_boolean
FROM
	extract_all
WHERE
	typ IN ('string', 'null', 'number', 'boolean');

This could then be stored in a separate table, which gives a lot more flexibility, for example you could have a generated tsvector column to allow for full text search of all json values.

To update the separate table you could set up a trigger to update whenever the source table changes.