Generate beautiful JSON from PostgreSQL

May 19, 2020

PostgreSQL provides a set of built-in JSON creation functions that can be used to build basic JSON structures. This increases the performance up to 10 times more than building it at the back-end layer.

This post is about building different JSON structures using PostgreSQL built-in functions. It doesn't talk about storing and manipulating JSON in PostgreSQL.

In order to proceed with some examples, first we need to setup a test database.

CREATE DATABASE jsonland

Let's create the following tables.

CREATE TABLE "user" (
  id SERIAL NOT NULL,
  name VARCHAR(100),
  email_address VARCHAR(150),
  PRIMARY KEY(id)
)

CREATE TABLE team (
  id SERIAL NOT NULL,
  name VARCHAR(100),
  PRIMARY KEY(id) 
)

CREATE TABLE team_user (
  id SERIAL NOT NULL,
  team_id INTEGER NOT NULL,
  user_id INTEGER NOT NULL,
  FOREIGN KEY(team_id) REFERENCES "team" (id),
  FOREIGN KEY(user_id) REFERENCES "user" (id),
  PRIMARY KEY(id) 
)

Let's Seed the tables with random data.

INSERT INTO "team" ("id", "name") 
VALUES (1, 'team1'), (2, 'team2');

INSERT INTO "user" ("id", "name", "email_address") 
VALUES (1, 'user1', 'user1@mail.com'), (2, 'user2', 'user2@mail.com');

INSERT INTO "team_user" ("id", "team_id", "user_id")
VALUES (1, 1, 1), (2, 1, 2), (3, 2, 2);

We have created three tables i.e. team, user and team_user. team_user table maps one-to-may the relationship between users and teams.

1. Get the table data as JSON objects

SELECT row_to_json("user") FROM "user";

+-----------------------------------------------------------+
| row_to_json                                               |
|-----------------------------------------------------------|
| {"id":1,"name":"user1","email_address":"user1@gmail.com"} |
| {"id":2,"name":"user2","email_address":"user2@gmail.com"} |
+-----------------------------------------------------------+

The above mentioned query will return all the columns of each row as JSON objects.

2. Get the table data with specific columns

We can specify the particular columns we need rather than getting all at once.

SELECT row_to_json(row('id', 'name')) FROM "user";

+-------------------------+
| row_to_json             |
|-------------------------|
| {"f1":"id","f2":"name"} |
| {"f1":"id","f2":"name"} |
+-------------------------+

Now certainly the keys f1 and f2 in the objects are not very useful to us. We would rather want the column names instead of those keys.

SELECT row_to_json(users) FROM (SELECT id, name FROM "user") AS users;

+-------------------------+
| row_to_json             |
|-------------------------|
| {"id":1,"name":"user1"} |
| {"id":2,"name":"user2"} |
+-------------------------+

3. Get the table data as a single JSON object

The above examples return us multiple JSON objects(one for each row). Ideally we would want a single array of these objects which won't need any further manipulation at back-end layer.

SELECT array_to_json(array_agg(row_to_json(users)))
	FROM (
		SELECT id, name from "user"
	) users

-- OR

SELECT json_agg(row_to_json(users))
	FROM (
		SELECT id, name from "user"
	) users

+----------------------------------------------------+
| json_agg                                           |
|----------------------------------------------------|
| [{"id":1,"name":"user1"}, {"id":2,"name":"user2"}] |
+----------------------------------------------------+

In the above query we are aggregating all the JSON objects and using array_agg and then converting them to JSON by applying array_to_json function.

Also we could do the yield the same results by using json_agg function, which results into an object instead of JSON string.

4. Build JSON object with multiple tables

We can also build a new JSON object by using json_build_object and specify the keys and values. Let's create an object that will contain data from both team and user table.

SELECT json_build_object(
  'users', (SELECT json_agg(row_to_json("user")) from "user"),
  'teams', (SELECT json_agg(row_to_json("team")) from "team")
)

This query generates a JSON structure that will have all the users and teams each as arrays of objects.

{
  "users": [
    {
      "id": 1,
      "name": "user1",
      "email_address": "user1@mail.com"
    },
    {
      "id": 2,
      "name": "user2",
      "email_address": "user2@mail.com"
    }
  ],
  "teams": [
    {
      "id": 1,
      "name": "team1"
    },
    {
      "id": 2,
      "name": "team2"
    }
  ]
}

5. Build JSON object by resolving foreign keys

We can generate JSON structures by resolving foreign key references and joining multiple tables.

select json_agg(row_to_json(tu)) 
	from (
		select id, (
			select row_to_json(team) from team where team_user.team_id = team.id
		) team, (
			select row_to_json("user") from "user" where team_user.user_id = "user".id
		) "user"
	from team_user
) tu

This query contains multiple sub-queries to generate a complex structure. It resolved the references of team_id and user_id into the corresponding row.

[
  {
    "id": 1,
    "team": {
      "id": 1,
      "name": "team1"
    },
    "user": {
      "id": 1,
      "name": "user1",
      "email_address": "user1@mail.com"
    }
  },
  {
    "id": 2,
    "team": {
      "id": 1,
      "name": "team1"
    },
    "user": {
      "id": 2,
      "name": "user2",
      "email_address": "user2@mail.com"
    }
  },
  {
    "id": 3,
    "team": {
      "id": 2,
      "name": "team2"
    },
    "user": {
      "id": 2,
      "name": "user2",
      "email_address": "user2@mail.com"
    }
  }
]

Conclusion

Even though PostgreSQL is almost always faster than the back-end language based JSON generation, the query can get complex really quickly as we have nested structures. As long as we understand the basic JSON functions and sub-queries we can build almost any kind of structure without stressing the back-end processes.