Skip to content

Instantly share code, notes, and snippets.

@villelahdenvuo
Created June 14, 2016 12:24
Show Gist options
  • Save villelahdenvuo/5b1fbbf3f22853475bba14f8833faada to your computer and use it in GitHub Desktop.
Save villelahdenvuo/5b1fbbf3f22853475bba14f8833faada to your computer and use it in GitHub Desktop.
PostgreSQL jsonb_set is nice, but it does't create missing objects so I made this little helper.
CREATE OR REPLACE FUNCTION jsonb_set_deep(target jsonb, path text[], val jsonb)
RETURNS jsonb AS $$
DECLARE
k text;
p text[];
BEGIN
-- Create missing objects in the path.
FOREACH k IN ARRAY path LOOP
p := p || k;
IF (target #> p IS NULL) THEN
target := jsonb_set(target, p, '{}'::jsonb);
END IF;
END LOOP;
-- Set the value like normal.
RETURN jsonb_set(target, path, val);
END;
$$ LANGUAGE plpgsql;
@wmwart
Copy link

wmwart commented Jul 20, 2022

Version that handles conditions:

  • if the target is initially null,
  • if path is null (insert to root)
CREATE OR REPLACE FUNCTION jsonb_set_deep(target jsonb, path text[], val jsonb)
  RETURNS jsonb AS $$
    DECLARE
      k text;
      p text[];
    BEGIN
      IF (path = '{}') THEN
        RETURN val;
      ELSE 
        IF (target IS NULL) THEN
          target = '{}'::jsonb;
        END IF;
        
        FOREACH k IN ARRAY path LOOP
          p := p || k;
          IF (target #> p IS NULL) THEN
            target := jsonb_set(target, p, '{}'::jsonb);
          ELSE 
            target := jsonb_set(target, p, target #> p);
          END IF;
        END LOOP;

        -- Set the value like normal.
        RETURN jsonb_set(target, path, val);
      END IF;
    END;
  $$ LANGUAGE plpgsql;

@jcalfee
Copy link

jcalfee commented Aug 31, 2023

It was pretty useful to include the ability to concatenate or merge objects (the || operator). Without that, the calling code is pretty verbose. This one should support arrays and act more like json_set. I'm not sure about older versions like pg9 but there are some short example SQLs included below.

/* 
  https://gist.github.com/villelahdenvuo/5b1fbbf3f22853475bba14f8833faada

  Handles conditions:
  * Accept array index in path
  * Concatenation (using the || operator) when target path matches a type in the "concat" array parameter like ARRAY['array', 'object', 'string'] (array append, object merge, strings into an array of strings, etc.).
  * If target is null, return null (like jsonb_set)
  * If path is null return null (like jsonb_set)
  * If path is empty return target (like jsonb_set)
  * If concat is null return null
*/
CREATE FUNCTION jsonb_set_deep(
  target jsonb, path text[], val jsonb, concat text[] = ARRAY['object']
)
  RETURNS jsonb AS $$
    DECLARE
      k text;
      p text[];
      i int = 1;
    BEGIN
      IF target IS NULL OR path IS NULL OR concat IS NULL THEN
        RETURN NULL; -- strict
      END IF;

      FOREACH k IN ARRAY path LOOP
        p = p || k;
        IF target #> p IS NULL THEN
          IF path[i + 1] ~ '^[0-9]+$' THEN
            target = jsonb_set(target, p, '[]'::jsonb);
          ELSE
            target = jsonb_set(target, p, '{}'::jsonb);
          END IF;
        END IF;
        i = i + 1;
      END LOOP;

      IF jsonb_typeof(target #> path) = ANY(concat) THEN
        RETURN jsonb_set(target, path, target #> path || val);
      ELSE
        RETURN jsonb_set(target, path, val);
      END IF;
    END;
  $$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE;

/*
  Accept array index in path
  concat is true:  {"a":{"b":1},"c":[1,2,{"d":3}]}
  concat is false: {"a":{"b":1},"c":[2,{"d":3}]}
*/
SELECT jsonb_set_deep(
  '{"a": {"b": 1}, "c":[1]}'::jsonb, ARRAY['c']::TEXT[],
  '[2, {"d": 3}]'::jsonb,
  ARRAY['object', 'array']
);

-- {"a": {"b": 1}, "c": ["abc", "def"]}
SELECT jsonb_set_deep(
  '{"a": {"b": 1}, "c": "abc"}'::jsonb,
  ARRAY['c']::TEXT[],
  '"def"'::jsonb,
  ARRAY['string']
);

/*
If target is null, return null (like jsonb_set)

?column?|?column?|
--------+--------+
true    |true    |
*/
SELECT jsonb_set(null, array['a']::text[], '1'::jsonb) IS NULL, 
  jsonb_set_deep(null, array['a']::text[], '1'::jsonb) IS NULL;

/*
If path is null return null (like jsonb_set)
 
 ?column?|?column?|
--------+--------+
true    |true    |
*/
SELECT jsonb_set('{}'::jsonb, null::text[], '1'::jsonb) IS NULL, 
  jsonb_set_deep('{}'::jsonb, null::text[], '1'::jsonb) IS NULL;

/*
If path is empty return target (like jsonb_set)

 jsonb_set|jsonb_set_deep|
---------+--------------+
{"a": 1} |{"a": 1}      |
*/
SELECT jsonb_set('{"a": 1}'::jsonb, array[]::text[], '{"a": 2}'::jsonb), 
  jsonb_set_deep('{"a": 1}'::jsonb, array[]::text[], '{"a": 2}'::jsonb);

/*
If concat is null return null
 
?column?|
--------+
true    |
*/
SELECT jsonb_set_deep('{}'::jsonb, array['a']::text[], '1'::jsonb, NULL) IS NULL;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment