How to compare two jsons ignoring order of elements in array properties?

23k Views Asked by At

I need to compare two strings which represent json objects. For testing purposes I need a way to compare these strings ignoring not only the child elements order (which is quite common) but order of elements in array properties of jsons. I.e.:

group: {
    id: 123,
    users: [
       {id: 234, name: John},
       {id: 345, name: Mike}
    ]
}

should be equal to:

group: {
    id: 123,
    users: [
       {id: 345, name: Mike},
       {id: 234, name: John}
    ]
}

Ideally I need some javascript lib, but other approaches welcome too.

7

There are 7 best solutions below

0
On

This answer describes a solution for the problem using the DeltaJSON REST API. DeltaJSON is a commercial product that provides the API either as a service (SaaS) or via a REST server that can be run locally:

  1. Start the DeltaJSON Rest Server (requires Java installation and a license file):
java -jar deltajson-rest-1.1.0.jar
  1. In your JavaScript, call the DeltaJSON REST API with the arrayAlignment property set to orderless.

The sample code below shows how to invoke the API with this property setting:

async function runTest() {
  const group1 = {
    id: 123,
    users: [
      { id: 234, name: "John" },
      { id: 345, name: "Mike" }
    ]
  };
  const group2 = {
    id: 123,
    users: [
      { id: 345, name: "Mikey" },
      { id: 234, name: "John" }
    ]
  };

  // call wrapper function that makes the REST API call:
  const isEqual = await compare(group1, group2);
  // log the comparison result: true
  console.log("isEqual", isEqual);
}

async function compare(aData, bData) {
  const aString = JSON.stringify(aData);
  const bString = JSON.stringify(bData);
  const blobOptions = { type: "application/json" };

  var formdata = new FormData();
  formdata.append("a", new Blob([aString], blobOptions));
  formdata.append("b", new Blob([bString], blobOptions));
  formdata.append("arrayAlignment", "orderless");

  const myHeaders = new Headers();
  myHeaders.append("Accept", "application/json");

  var requestOptions = {
    method: "POST",
    headers: myHeaders,
    body: formdata,
    redirect: "follow"
  };

  try {
    const response = await fetch(
      "http://localhost:8080/api/json/v1/compare",
      requestOptions
    );
    const jsonObj = await response.json();
    console.log(jsonObj);
    const dataSets = jsonObj.dx_deltaJSON.dx_data_sets;
    const isEqual = dataSets === "A=B";
    return isEqual;
  } catch (e) {
    console.error(e);
  }
}

// run the test:
runTest(); // true

Explanation:

The DeltaJSON Rest API response is an annotated form of the JSON inputs. Extra dx_ prefixed properties are added to describe the changes. A metadata dx_deltaJSON property is also included in the JSON.

The value of the dx_deltaJSON property is an object that has a dx_data_sets property that we can test to see (in a two-way comparison) that the value is A=B.

Here is the result where there are slightly different inputs to that in the question. Here, as well as the order of the array items being changed, 'Mike' has been changed to 'Mikey':

{
  "dx_deltaJSON": {
    "dx_data_sets": "A!=B",
    "dx_deltaJSON_type": "diff",
    "dx_deltaJSON_metadata": {
      "operation": {
        "type": "compare",
        "input-format": "multi_part",
        "output-format": "JSON"
      },
      "parameters": {
        "dxConfig": [],
        "arrayAlignment": "orderless",
        "wordByWord": false
      }
    },
    "dx_deltaJSON_delta": {
      "id": 123,
      "users": [
        {
          "id": 345,
          "name": {
            "dx_delta": {
              "A": "Mike",
              "B": "Mikey"
            }
          }
        },
        {
          "id": 234,
          "name": "John"
        }
      ]
    }
  }
}
0
On

I don't know if such thing exist, but you can implement it yourself.

var group1 = {
    id: 123,
    users: [
       {id: 234, name: "John"},
       {id: 345, name: "Mike"}
    ]
};

var group2 = {
    id: 123,
    users: [
       {id: 345, name: "Mike"},
       {id: 234, name: "John"}
    ]
};

function equal(a, b) {

    if (typeof a !== typeof b) return false;
    if (a.constructor !== b.constructor) return false;

    if (a instanceof Array)
    {
        return arrayEqual(a, b);
    }

    if(typeof a === "object")
    {
        return objectEqual(a, b);
    }

    return a === b;
}

function objectEqual(a, b) {
    for (var x in a)
    {
         if (a.hasOwnProperty(x))
         {
             if (!b.hasOwnProperty(x))
             {
                 return false;
             }

             if (!equal(a[x], b[x]))
             {
                 return false;
             }
         }
    }

    for (var x in b)
    {
        if (b.hasOwnProperty(x) && !a.hasOwnProperty(x))
        {
            return false;
        }
    }

    return true;
}

function arrayEqual(a, b) {
    if (a.length !== b.length)
    {
        return false;
    }

    var i = a.length;

    while (i--)
    {
        var j = b.length;
        var found = false;

        while (!found && j--)
        {
            if (equal(a[i], b[j])) found = true;
        }

        if (!found)
        {
            return false;
        }
    }

    return true;
}

alert(equal(group1, group2))
0
On

You could slice the arrays, sort them by Id then stringify them to JSON and compare the strings. For a lot of members it should work pretty fast. If you duplicate Ids, it will fail because sort will not change the order.

2
On

Here is my attempt at a custom implementation:

var equal = (function(){
  function isObject(o){
    return o !== null && typeof o === 'object';
  }
  return function(o1, o2){
    if(!isObject(o1) || !isObject(o2)) return o1 === o2;
    var key, allKeys = {};
    for(key in o1)
      if(o1.hasOwnProperty(key))
        allKeys[key] = key;
    for(key in o2)
      if(o2.hasOwnProperty(key))
        allKeys[key] = key;
    for(key in allKeys){
      if(!equal(o1[key], o2[key])) return false;
    }
    return true;
  }
})();

An example of it with test cases:

var p1 = {
  tags: ['one', 'two', 'three'],
  name: 'Frank',
  age: 24,
  address: {
    street: '111 E 222 W',
    city: 'Provo',
    state: 'Utah',
    zip: '84604'
  }
}
var p2 = {
  name: 'Frank',
  age: 24,
  tags: ['one', 'two', 'three'],
  address: {
    street: '111 E 222 W',
    city: 'Provo',
    state: 'Utah',
    zip: '84604'
  }
}
var p3 = {
  name: 'Amy',
  age: 24,
  tags: ['one', 'two', 'three'],
  address: {
    street: '111 E 222 W',
    city: 'Provo',
    state: 'Utah',
    zip: '84604'
  }
}
var p4 = {
  name: 'Frank',
  age: 24,
  tags: ['one', 'two', 'three'],
  address: {
    street: '111 E 222 W',
    city: 'Payson',
    state: 'Utah',
    zip: '84604'
  }
}
var p5 = {
  name: 'Frank',
  age: 24,
  tags: ['one', 'two'],
  address: {
    street: '111 E 222 W',
    city: 'Provo',
    state: 'Utah',
    zip: '84604'
  }
}

var equal = (function(){
  function isObject(o){
    return o !== null && typeof o === 'object';
  }
  return function(o1, o2){
    if(!isObject(o1) || !isObject(o2)) return o1 === o2;
    var key, allKeys = {};
    for(key in o1)
      if(o1.hasOwnProperty(key))
        allKeys[key] = key;
    for(key in o2)
      if(o2.hasOwnProperty(key))
        allKeys[key] = key;
    for(key in allKeys){
      if(!equal(o1[key], o2[key])) return false;
    }
    return true;
  }
})();

var cases = [
  {name: 'Compare with self', a: p1, b: p1, expected: true},
  {name: 'Compare with identical', a: p1, b: p2, expected: true},
  {name: 'Compare with different', a: p1, b: p3, expected: false},
  {name: 'Compare with different (nested)', a: p1, b: p4, expected: false},
  {name: 'Compare with different (nested array)', a: p1, b: p5, expected: false}
];

function runTests(tests){
  var outEl = document.getElementById('out');
  for(var i=0; i < tests.length; i++){
    var actual = equal(tests[i].a, tests[i].b),
        result = tests[i].expected == actual
          ? 'PASS'
          : 'FAIL';
    outEl.innerHTML += 
      '<div class="test ' + result + '">' + 
        result + ' ' +
        tests[i].name + 
      '</div>';
  }
}
runTests(cases);
body{
  font-family:monospace;
}
.test{
  margin:5px;
  padding:5px;  
}
.PASS{
  background:#EFE;
  border:solid 1px #32E132;
}
.FAIL{
  background:#FEE;  
  border:solid 1px #FF3232;
}
<div id=out></div>

0
On

I'm using object-hash

But I'm not sure if is performant for production code


// import * as hash from 'object-hash'
const hash = require('object-hash')

const objA = {
  id: 123,
  users: [
     {id: 234, name: "John"},
     {id: 345, name: "Mike"}
  ]
}

const objB = {
  id: 123,
  users: [
     {id: 345, name: "Mike"},
     {id: 234, name: "John"}
  ]
}

const options = {unorderedArrays: true}

hash(objA, options) == hash(objB, options) //true
3
On

Use JSONAssert

They have a loose assert.

Loose:

JSONAssert.assertEquals(exp, act, false);

Strict:

JSONAssert.assertEquals(exp, act, true);
0
On

I like the solution of Francis and it workes very well.

Just add the following null check at the beginning of the equal function to prevent errors with null or undefined inputs..

if (a == null && b == null) {
  return true;
}
if (a == null || b == null) {
  return false;
}

So the whole solution would look something like:

function equal(a, b) {
    if (a == null && b == null) {
      return true;
    }
    if (a == null || b == null) {
      return false;
    }
    if (typeof a !== typeof b) return false;
    if (a.constructor !== b.constructor) return false;

    if (a instanceof Array)
    {
        return arrayEqual(a, b);
    }

    if(typeof a === "object")
    {
        return objectEqual(a, b);
    }

    return a === b;
}

function objectEqual(a, b) {
    for (var x in a)
    {
         if (a.hasOwnProperty(x))
         {
             if (!b.hasOwnProperty(x))
             {
                 return false;
             }

             if (!equal(a[x], b[x]))
             {
                 return false;
             }
         }
    }

    for (var x in b)
    {
        if (b.hasOwnProperty(x) && !a.hasOwnProperty(x))
        {
            return false;
        }
    }

    return true;
}

function arrayEqual(a, b) {
    if (a.length !== b.length)
    {
        return false;
    }

    var i = a.length;

    while (i--)
    {
        var j = b.length;
        var found = false;

        while (!found && j--)
        {
            if (equal(a[i], b[j])) found = true;
        }

        if (!found)
        {
            return false;
        }
    }

    return true;
}