12

I have two nested objects obj1 and obj2 and I want to compare them and the recursively return an object that for each nested key has a equality-like boolean flag

So for a given obj1 like

obj1 = {
  prop1: 1,
  prop2: "foo",
  prop3: {
    prop4: 2,
    prop5: "bar" 
  }
}

and the obj2 like

obj2 = {
      prop1: 3,
      prop2: "foo",
      prop3: {
        prop4: 2,
        prop5: "foobar" 
      },
      prop6: "new"
    }

it should return

equality = {
     prop1: false,
     prop2: true,
     prop3 : {
       prop4: true,
       prop5: false
     },
     prop6: false
   }

If an object has a new property, like obj2.prop6, then the equality will be equality.prop6 = false.

For non-nested object a simple keys comparison solutions is here Get the property of the difference between two objects in javascript While to recursively compare nested objects it is showed here JavaScript: Deep comparison recursively: Objects and properties

loretoparisi
  • 15,724
  • 11
  • 102
  • 146
  • 1
    Will both objects always have exact match properties? – holydragon Apr 09 '19 at 10:59
  • good point. Nope, so the equality could have a new key set to `false`. Updating with this point. Thank you. – loretoparisi Apr 09 '19 at 11:00
  • 1
    A contributor refer this link in his question, And he has an arrays of deep nested object as inputs, So I thought, I should refer my [answer here](https://stackoverflow.com/questions/67206511/compare-nested-objects-with-arrays-in-javascript-and-return-keys-equality?noredirect=1#comment118813374_67206511) – Nur Apr 22 '21 at 17:59

5 Answers5

10

You could iterate all keys and check the nested objects if both values are objects.

const isObject = v => v && typeof v === 'object';

function getDifference(a, b) {
    return Object.assign(...Array.from(
        new Set([...Object.keys(a), ...Object.keys(b)]),
        k => ({ [k]: isObject(a[k]) && isObject(b[k])
            ? getDifference(a[k], b[k])
            : a[k] === b[k]
        })
    ));
}

var obj1 = { prop1: 1, prop2: "foo", prop3: { prop4: 2, prop5: "bar" } },
    obj2 = { prop1: 3, prop2: "foo", prop3: { prop4: 2, prop5: "foobar" }, prop6: "new" };

console.log(getDifference(obj1, obj2));
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • Thanks! This solution works in every condition even if the property is a void object like `prop7 = {}`. Maybe avoiding the ECMA6 `Spread` notation for more compatibility could help. – loretoparisi Apr 09 '19 at 12:30
  • maybe like `Object.assign(Array.from(new Set( [].concat(Object.keys(a)).concat(Object.keys(b)))` – loretoparisi Apr 09 '19 at 12:35
4

You could use reduce to build new object and another get method to get nested props from other object by string and compare it to current prop value in first object.

const obj1 = { prop1: 1, prop2: "foo", prop3: { prop4: 2, prop5: "bar" } }
const obj2 = { prop1: 3, prop2: "foo", prop3: { prop4: 2, prop5: "foobar" } }

function get(obj, path) {
  return path.split('.').reduce((r, e) => {
    if (!r) return r
    else return r[e] || undefined
  }, obj)
}

function compare(a, b, prev = "") {
  return Object.keys(a).reduce((r, e) => {
    const path = prev + (prev ? '.' + e : e);
    const value = a[e] === get(b, path);
    r[e] = typeof a[e] === 'object' ? compare(a[e], b, path) : value
    return r;
  }, {})
}

const result = compare(obj1, obj2);
console.log(result)

To compare all properties of both objects you could create extra function that will perform loop by both objects.

const obj1 = {"prop1":1,"prop2":"foo","prop3":{"prop4":2,"prop5":"bar"},"prop7":{"prop9":{"prop10":"foo"}}}
const obj2 = {"prop1":3,"prop2":"foo","prop3":{"prop4":2,"prop5":"foobar"},"prop6":"new","prop7":{"foo":"foo","bar":{"baz":"baz"}}}

function get(obj, path) {
  return path.split('.').reduce((r, e) => {
    if (!r) return r;
    else return r[e] || undefined;
  }, obj);
}

function isEmpty(o) {
  if (typeof o !== 'object') return true;
  else return !Object.keys(o).length;
}

function build(a, b, o = null, prev = '') {
  return Object.keys(a).reduce(
    (r, e) => {
      const path = prev + (prev ? '.' + e : e);
      const bObj = get(b, path);
      const value = a[e] === bObj;

      if (typeof a[e] === 'object') {
        if (isEmpty(a[e]) && isEmpty(bObj)) {
          if (e in r) r[e] = r[e];
          else r[e] = true;
        } else if (!bObj && isEmpty(a[e])) {
          r[e] = value;
        } else {
          r[e] = build(a[e], b, r[e], path);
        }
      } else {
        r[e] = value;
      }
      return r;
    },
    o ? o : {}
  );
}

function compare(a, b) {
  const o = build(a, b);
  return build(b, a, o);
}

const result = compare(obj1, obj2);
console.log(result)
Nenad Vracar
  • 118,580
  • 15
  • 151
  • 176
  • 1
    I thinks this is the best solution since it supports all recent version of ECMAScript. – loretoparisi Apr 09 '19 at 12:23
  • Just a thing, supposed that the property is a void object like `{}`, it does not make the comparison. – loretoparisi Apr 09 '19 at 12:29
  • Ok, now it handles a nested lonely `prop11={}` object, but if you have like the same `prop12={}` in both, you will get the `{}` as result for the `prop12` keyword, instead of `bool`. See here https://jsfiddle.net/gpu20nwy/ – loretoparisi Apr 09 '19 at 12:50
  • This works, please update solution, so that I can accept it! – loretoparisi Apr 09 '19 at 13:26
3

You could create a merged object which will have keys of both object. Loop through this object and compare the values for both obj1 and obj2 for each key. If the property is an object, recursively compare the properties. This will work for any level of nesting. Since the properties could be missing from either of the objects, default parameter = {} is added.

const obj1={prop1:1,prop2:"foo",prop3:{prop4:2,prop5:"bar"},prop7:{pro8:"only in 1"}},
      obj2={prop1:3,prop2:"foo",prop3:{prop4:2,prop5:"foobar"}, prop6: "only in 2"};
    
const isObject = val => typeof val === 'object' && val // required for "null" comparison

function compare(obj1 = {}, obj2 = {}) {
  const output = {},
        merged = { ...obj1, ...obj2 }; // has properties of both

  for (const key in merged) {
    const value1 = obj1[key],
          value2 = obj2[key];

    if (isObject(value1) || isObject(value2))
      output[key] = compare(value1, value2) // recursively call
    else
      output[key] = value1 === value2
  }
  
  return output;
}

console.log(compare(obj1, obj2))
adiga
  • 34,372
  • 9
  • 61
  • 83
  • Thanks, just updated the code, assuming you can have new properties as well. – loretoparisi Apr 09 '19 at 11:05
  • what if obj1 have some key with a child obj as its value and obj2 does not have the same key, then the method will be called with compare(obj1, undefined) which will throw an error at obj2[key] – AZ_ Apr 09 '19 at 11:09
  • @adiga not sure, but the comments in the question do mention that, and the updated question as well, it cant be assumed that extra ket always have a string value. – AZ_ Apr 09 '19 at 11:18
  • @AZ_ updated. Not sure if it will fail for any scenario – adiga Apr 09 '19 at 11:27
1

A recursive example,

var obj1 = {
        prop1: 1,
        prop2: "foo",
        prop3: {
            prop4: 2,
            prop5: "bar"
        },
        prop7: {},
    }

    var obj2 = {
        prop1: 3,
        prop2: "foo",
        prop3: {
            prop4: 2,
            prop5: "foobar"
        },
        prop6: "new",
        prop7: {},
        prop8: {},
    }

    var result = {};

    function compare(obj1, obj2, obj_) {
        for (let k in obj1) {
            var type = typeof obj1[k];
            if (type === 'object') {
                obj_[k] = {};
                if (!obj2[k]){
                    obj_[k] = false;
                }else if ((Object.entries(obj1[k]).length === 0 && obj1[k].constructor === Object) && (Object.entries(obj2[k]).length === 0 && obj2[k].constructor === Object)) {
                    obj_[k] = true;
                } else {
                    compare(obj1[k], obj2[k], obj_[k]);
                }
            } else {
                obj_[k] = (obj1[k] === obj2[k]);
            }

        }
    }

    if (Object.keys(obj1).length < Object.keys(obj2).length) { //check if both objects vary in length.
        var tmp = obj1;
        obj1 = obj2;
        obj2 = tmp;
    }

    compare(obj1, obj2, result);

    console.log(result);
Shoyeb Sheikh
  • 2,659
  • 2
  • 10
  • 19
1

Here is a solution that I made recently that can handle the same issue, and it takes an optional key for setting comparison strictness. Useful for when your backend sends a value as a number, but expects that value to be returned as a string. We had been using JSON.stringify comparison, but it is a bit crude, and cant account for when the objects are the same but the keys are in different order.

public objectsAreTheSame(objA: {[key: string]: any}, objB: {[key: string]: any}, isStrict = false): boolean {
        let areTheSame = true;

        const strictLevel = isStrict ? 'isStrict' : 'isNotStrict';

        const valuesDoNotMatch = {
            'isStrict': (a, b) => a !== b,
            'isNotStrict': (a, b) => a != b
        };

        const isObject = (a, b) => typeof a === 'object' && !Array.isArray(a) && (!!a && !!b);

        const compareArrays = (a, b) => {
            if (a.length === b.length) {
                a.sort();
                b.sort();

                a.forEach((ele, idx) => compareValues(ele, b[idx]));
            } else {
                areTheSame = false;
            }
        };

        const compareValues = (a, b) => {
            if (Array.isArray(a)) {
                compareArrays(a, b);
            } else if (!isObject(a, b) && valuesDoNotMatch[strictLevel](a, b)) {
                areTheSame = false;
            } else if (isObject(a, b) && !this.objectsAreTheSame(a, b, isStrict)) {
                areTheSame = false;
            }
        };

        const keysA = Object.keys(objA);
        const keysB = Object.keys(objB);

        if (keysA.length !== keysB.length) return false;

        for (let key of keysA) compareValues(objA[key], objB[key]);

        return areTheSame;
    }
brooklynDadCore
  • 1,309
  • 8
  • 13