Modify Nested Arrays in PostgreSQL JSON/B Columns

Postgres does not provide modification operations for nested JSON arrays out-of-the-box. By combining some JSON utility functions with array functions you can achieve dynamic update statements to do adding/removing by value.

Example

Let's create a simple user table with an id, username and metadata attribute which is of type jsonb:

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TABLE users (
    id UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(),
    username TEXT NOT NULL,
    meta_data JSONB NOT NULL DEFAULT '{}'::jsonb
);

INSERT INTO users (username, meta_data)
VALUES ('johndoe', '{"roles": ["hr", "marketing", "sales"]}'::jsonb);

SELECT id, username, meta_data
FROM users;

Which will print:

d9764861-f555-4e77-8f56-6c15b0d48136,johndoe,"{""roles"": [""hr"", ""marketing"", ""sales""]}"

Adding Elements

Let's add the "development" and "administration" roles to the users roles array:

UPDATE users
SET meta_data = jsonb_set(meta_data, ARRAY['roles'],
    COALESCE(meta_data->'roles', '[]'::jsonb) || array_to_json(ARRAY['development', 'administration'])::jsonb)
WHERE id = 'd9764861-f555-4e77-8f56-6c15b0d48136';

Adding is rather simple. The function jsonb_set takes as first parameter the json structure you want to update, as second parameter a PostgreSQL array of identifiers/indices leading to the part to update and as third parameter the new value. You could even address the first role of the user and replace it by a new value:

UPDATE users
SET meta_data = jsonb_set(meta_data, ARRAY['roles', 0], 'development')::jsonb)
WHERE id = 'd9764861-f555-4e77-8f56-6c15b0d48136';

Removing Element

Removing the two roles is a bit more complicate:

UPDATE users
SET meta_data = jsonb_set(meta_data, ARRAY['roles'],
    array_to_json(
        array_remove(
            array(SELECT jsonb_array_elements_text(meta_data->'roles')),
            'administration')
        )::jsonb)
WHERE id = 'd9764861-f555-4e77-8f56-6c15b0d48136';

Postgres arrays offer a utility function to remove elements by value called array_remove(anyarray, text). Therefore we need to convert our JSON array to a Postgres type and then back. Converting to the Postgres type is more complicated. The function jsonb_array_elements_text converts any JSON array to a result set which can be passed to the array() constructor. Afterwards we can apply array_remove and convert it back with array_to_json.