13

As known, at the moment PostgreSQL has no method to compare two json values. The comparison like json = json doesn't work. But what about casting json to text before?

Then

select ('{"x":"a", "y":"b"}')::json::text = 
('{"x":"a", "y":"b"}')::json::text

returns true

while

select ('{"x":"a", "y":"b"}')::json::text = 
('{"x":"a", "y":"d"}')::json::text

returns false

I tried several variants with more complex objects and it works as expected.

Are there any gotchas in this solution?

UPDATE:

The compatibility with v9.3 is needed

MapUser
  • 351
  • 1
  • 3
  • 15

3 Answers3

14

You can also use the @> operator. Let's say you have A and B, both JSONB objects, so A = B if:

A @> B AND A <@ B

Read more here: https://www.postgresql.org/docs/current/functions-json.html

Pika Supports Ukraine
  • 3,612
  • 10
  • 26
  • 42
Abel Osorio
  • 843
  • 8
  • 13
  • that only works with jsonb and MapUser wrote that he/she is stuck limited to Postgres 9.3 which doesn't support JSONB –  Apr 15 '19 at 21:00
  • 2
    That only works with simple objects, but not with objects containing arrays or jsonb-arrays directly. See my answer for a comprehensive solution. – Overbryd Nov 11 '19 at 13:27
10

Yes there are multiple problem with your approach (i.e. converting to text). Consider the following example

select ('{"x":"a", "y":"b"}')::json::text = ('{"y":"b", "x":"a"}')::json::text;

This is like your first example example, except that I flipped the order of the x and y keys for the second object, and now it returns false, even thought the objects are equal.

Another issue is that json preserves white space, so

select ('{"x":"a", "y":"b"}')::json::text = ('{ "x":"a", "y":"b"}')::json::text;

returns false just because I added a space before the x in the second object.

A solution that works with v9.3 is to use the json_each_text function to expand the two JSON objects into tables, and then compare the two tables, e.g. like so:

SELECT NOT exists(
    SELECT
    FROM json_each_text(('{"x":"a", "y":"b"}')::json) t1
         FULL OUTER JOIN json_each_text(('{"y":"b", "x":"a"}')::json) t2 USING (key)
    WHERE t1.value<>t2.value OR t1.key IS NULL OR t2.key IS NULL
)

Note that this only works if the two JSON values are objects where for each key, the values are strings.

The key is in the query inside the exists: In that query we match all keys from the first JSON objects with the corresponding keys in the second JSON object. Then we keep only the rows that correspond to one of the following two cases:

  • a key exists in both JSON objects but the corresponding values are different
  • a key exists only in one of the two JSON objects and not the other

These are the only cases that "witness" the inequality of the two objects, hence we wrap everything with a NOT exists(...), i.e. the objects are equal if we didn't find any witnesses of inequality.

If you need to support other types of JSON values (e.g. arrays, nested objects, etc), you can write a plpgsql function based on the above idea.

redneb
  • 21,794
  • 6
  • 42
  • 54
  • Thanks @redneb for detail clarification! – MapUser Mar 20 '18 at 21:43
  • I needed a function that compares any jsonb value to another jsonb value, see my answer for a solution that correctly compares two jsonb-type values (objects, arrays and literals). – Overbryd Nov 11 '19 at 13:28
4

Most notably A @> B AND B @> A will signify TRUE if they are both equal JSONB objects.

However, be careful when assuming that it works for all kinds of JSONB values, as demonstrated with the following query:

select
    old,
    new,
    NOT(old @> new AND new @> old) as changed
from (
    values
        (
        '{"a":"1", "b":"2", "c": {"d": 3}}'::jsonb,
        '{"b":"2", "a":"1", "c": {"d": 3, "e": 4}}'::jsonb
        ),
        (
        '{"a":"1", "b":"2", "c": {"d": 3, "e": 4}}'::jsonb,
        '{"b":"2", "a":"1", "c": {"d": 3}}'::jsonb
        ),
        (
        '[1, 2, 3]'::jsonb,
        '[3, 2, 1]'::jsonb
        ),
        (
        '{"a": 1, "b": 2}'::jsonb,
        '{"b":2, "a":1}'::jsonb
        ),
        (
        '{"a":[1, 2, 3]}'::jsonb,
        '{"b":[3, 2, 1]}'::jsonb
        )
) as t (old, new)

Problems with this approach are that JSONB arrays are not compared correctly, as in JSON [1, 2, 3] != [3, 2, 1] but Postgres returns TRUE nevertheless.

A correct solution will recursively iterate through the contents of the json and comparing arrays and objects differently. I have quickly built a set of functions that accomplishes just that.

Use them like SELECT jsonb_eql('[1, 2, 3]'::jsonb, '[3, 2, 1]'::jsonb) (the result is FALSE).

CREATE OR REPLACE FUNCTION jsonb_eql (a JSONB, b JSONB) RETURNS BOOLEAN AS $$
DECLARE
BEGIN
  IF (jsonb_typeof(a) != jsonb_typeof(b)) THEN
    RETURN FALSE;
  ELSE
    IF (jsonb_typeof(a) = 'object') THEN
      RETURN jsonb_object_eql(a, b);
    ELSIF (jsonb_typeof(a) = 'array') THEN
      RETURN jsonb_array_eql(a, b);
    ELSIF (COALESCE(jsonb_typeof(a), 'null') = 'null') THEN
      RETURN COALESCE(a, 'null'::jsonb) = 'null'::jsonb AND COALESCE(b, 'null'::jsonb) = 'null'::jsonb;
    ELSE
      RETURN coalesce(a = b, FALSE);
    END IF;
  END IF;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION jsonb_object_eql (a JSONB, b JSONB) RETURNS BOOLEAN AS $$
DECLARE
   _key_a text;
   _val_a jsonb;
   _key_b text;
   _val_b jsonb;
BEGIN
  IF (jsonb_typeof(a) != jsonb_typeof(b)) THEN
    RETURN FALSE;
  ELSIF (jsonb_typeof(a) != 'object') THEN
    RETURN jsonb_eql(a, b);
  ELSE
    FOR _key_a, _val_a, _key_b, _val_b IN
      SELECT t1.key, t1.value, t2.key, t2.value FROM jsonb_each(a) t1
      LEFT OUTER JOIN (
        SELECT * FROM jsonb_each(b)
      ) t2 ON (t1.key = t2.key)
    LOOP
      IF (_key_a != _key_b) THEN
        RETURN FALSE;
      ELSE
        RETURN jsonb_eql(_val_a, _val_b);
      END IF;
    END LOOP;
    RETURN a = b;
  END IF;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION jsonb_array_eql (a JSONB, b JSONB) RETURNS BOOLEAN AS $$
DECLARE
   _val_a jsonb;
   _val_b jsonb;
BEGIN
  IF (jsonb_typeof(a) != jsonb_typeof(b)) THEN
    RETURN FALSE;
  ELSIF (jsonb_typeof(a) != 'array') THEN
    RETURN jsonb_eql(a, b);
  ELSE
    FOR _val_a, _val_b IN
      SELECT jsonb_array_elements(a), jsonb_array_elements(b)
    LOOP
      IF (NOT(jsonb_eql(_val_a, _val_b))) THEN
        RETURN FALSE;
      END IF;
    END LOOP;
    RETURN TRUE;
  END IF;
END;
$$ LANGUAGE plpgsql;
Overbryd
  • 4,612
  • 2
  • 33
  • 33
  • 1
    JSONB arrays **are** compared correctly - [Db<>fiddle.](https://dbfiddle.uk/?rdbms=postgres_9.4&fiddle=9125e26121390a7f79525e1129fa5afe) – klin Nov 15 '19 at 10:20
  • To be honest my Postgres returned the wrong answer to me. I did first think it was an issue with arrays, but then realised something went off (other queries started returning weird results too) with my instance. After restarting the docker image, it returned the right answers again. So the issue I was describing in my answer might just be a local one, I will update it accordingly. – Overbryd Nov 16 '19 at 11:33
  • Thx, nice code example to work with here! – Wellspring Jan 25 '21 at 21:26
  • One thing worth noting about how `@>` works is that it'll compare arrays without ordering. That may be the desired outcome, but indeed `'[1, 2, 3]'::jsonb != '[3, 2, 1]'::jsonb` (yet `'[1, 2, 3]'::jsonb @> '[3, 2, 1]'::jsonb AND '[3, 2, 1]'::jsonb @> '[1, 2, 3]'::jsonb`. – sleighty Aug 14 '23 at 17:00