50

Scenario: I want a function that compares two JSON-objects, and returns a JSON-object with a list of the differences and if possible more data such as coverage metrics.

var madrid = '{"type":"team","description":"Good","trophies":[{"ucl":"10"}, {"copa":"5"}]}';
var barca = '{"type":"team","description":"Bad","trophies":[{"ucl":"3"}]}';

If i ran compare(madrid, barca) the returned object could look something like:

{"description" : "Bad", "trophies":[{"ucl":"3"}, {"copa":"5"}]}; 

Or something similar, you get the idea.

Does anyone know of a solution to this? I've already found one plugin, but I'd like to know if there are any alternatives.

Soroush Hakami
  • 5,226
  • 15
  • 66
  • 99
  • 3
    When you say "JSON-objects", do you really just mean "objects" or do you have the data actually in JSON (string) form? Also, could you give more detail about your expected output? Differences could mean "in A but not B", "in B but not A", or "in both but different". How would you want to report on nested objects where the only difference is several levels down? – nnnnnn Dec 08 '11 at 13:32
  • I've updated the initial post with more info! – Soroush Hakami Dec 08 '11 at 13:54

8 Answers8

61

It's possible to use a recursive function that iterates by the object keys. Then use the Object.is to test for NaN and null. Then test if the second object is the type that cast to false like 0, NaN, or null. List the keys of both objects and concatenate them to test of missing keys in the obj1 and then iterate it.

When there is a difference between the same key values, it stores the value of object2 and proceeds. If both key values are object means that can be recursively compared and so it does.

function diff(obj1, obj2) {
    const result = {};
    if (Object.is(obj1, obj2)) {
        return undefined;
    }
    if (!obj2 || typeof obj2 !== 'object') {
        return obj2;
    }
    Object.keys(obj1 || {}).concat(Object.keys(obj2 || {})).forEach(key => {
        if(obj2[key] !== obj1[key] && !Object.is(obj1[key], obj2[key])) {
            result[key] = obj2[key];
        }
        if(typeof obj2[key] === 'object' && typeof obj1[key] === 'object') {
            const value = diff(obj1[key], obj2[key]);
            if (value !== undefined) {
                result[key] = value;
            }
        }
    });
    return result;
}

The code above is BSD licensed and can be used anywhere.

Test link: https://jsfiddle.net/gartz/vy9zaof2/54/

An important observation, this will convert arrays to objects and compare the values in the same index position. There are many other ways to compare arrays not covered by this function due to the required extra complexity.

EDIT 2/15/2019: This answer was changed to add the new ES2017 syntax and fix use-cases from comments.


This is just a kickoff, I haven't tested it, but I began with a filter or comparator function, that is recursive, change it however you need to get priority results.

function filter(obj1, obj2) {
    var result = {};
    for(key in obj1) {
        if(obj2[key] != obj1[key]) result[key] = obj2[key];
        if(typeof obj2[key] == 'array' && typeof obj1[key] == 'array') 
            result[key] = arguments.callee(obj1[key], obj2[key]);
        if(typeof obj2[key] == 'object' && typeof obj1[key] == 'object') 
            result[key] = arguments.callee(obj1[key], obj2[key]);
    }
    return result;
}

Tests: http://jsfiddle.net/gartz/Q3BtG/2/

Gabriel Gartz
  • 2,840
  • 22
  • 24
  • 1
    Thanks for your reply. I tried it, but I can't really make use of the returned object. http://jsfiddle.net/zorro/Q3BtG/ – Soroush Hakami Dec 08 '11 at 15:36
  • Emil, my function is working I tested in jsfiddle, you have typed var as string not object, if you remove the first and last ' it will work as spected. And fix the arguments.callee stead arguments.calee http://jsfiddle.net/gartz/Q3BtG/2/ – Gabriel Gartz Dec 08 '11 at 16:00
  • seems there is a bug, if a value in the json has a null-value. then there is an empty json-object as changed element – simon Nov 20 '16 at 08:09
  • 4
    I updated this example using ES6 and removing the `argument.callee` statements if anyone might find that useful: https://jsfiddle.net/thinkolson/n2jwv6mk/2/ – Will Oct 09 '17 at 17:53
  • It's failing , try Example: var jsonObject1 = { name: 'my object', description: 'it\'s an object!', lead:[{"user_id":8,"is_active":1}] }; var jsonObject2 = { name: 'my objectd', description: 'it\'s an object!', lead:[{"user_id":8,"is_active":1}] }; – Aakash Kag Sep 04 '18 at 15:42
  • if ( obj1[key] === null && obj2[key] === null ) continue; // add this to the for loop to skip nulls which are special objects. are there any other special objects in JS? // this addresses the bug simon pointed out. – Gidon Wise Oct 01 '18 at 19:36
  • There are other issues. Like the fact that it doesn't compare all keys. var keys = Object.keys( obj1 ).concat( Object.keys(obj2) ); I would carefully QA before using this function. – Gidon Wise Oct 07 '18 at 08:00
  • Thanks for the comments, I've updated the function to cover those concerns about `argument.callee` depreciation, the usage of `null`, `NaN`, and when obj2 contains keys that obj1 doesn't. – Gabriel Gartz Feb 15 '19 at 19:43
11

contributing back my changes to Gabriel Gartz version. This one works in strict mode and removes the array check - will always be false. It also removes empty nodes from the diff.

//http://stackoverflow.com/questions/679915/how-do-i-test-for-an-empty-javascript-object
var isEmptyObject = function(obj) {
    var name;
    for (name in obj) {
        return false;
    }
    return true;
};

//http://stackoverflow.com/questions/8431651/getting-a-diff-of-two-json-objects
var diff = function(obj1, obj2) {
    var result = {};
    var change;
    for (var key in obj1) {
        if (typeof obj2[key] == 'object' && typeof obj1[key] == 'object') {
            change = diff(obj1[key], obj2[key]);
            if (isEmptyObject(change) === false) {
                result[key] = change;
            }
        }
        else if (obj2[key] != obj1[key]) {
            result[key] = obj2[key];
        }
    }
    return result;
};
Andrii Abramov
  • 10,019
  • 9
  • 74
  • 96
Leblanc Meneses
  • 3,001
  • 1
  • 23
  • 26
  • Much better. But why not use getOwnPropertyNames() instead of for..in? https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames – whitneyland Nov 21 '17 at 19:10
  • Sorry but I tried it and think it needs more work. The biggest problem is the results should be the same if you swap the order of the object parameters. You shouldn't be able to have "differrent", differences between two objects. Also it seems to focus on detecting value diffs, but which properties have been added / removed is kind of important in many cases. However thanks again for your work. Example: These two calls should return the same results: console.log(diff({a: 1}, {a: 10, b: 2, c: 3})) console.log(diff({a: 10, b: 2, c: 3}, {a: 1})) – whitneyland Nov 21 '17 at 19:25
8

You can use rus-diff https://github.com/mirek/node-rus-diff which creates MongoDB compatible (rename/unset/set) diff:

// npm install rus-diff
var madrid = {"type":"team","description":"Good","trophies":[{"ucl":"10"}, {"copa":"5"}]};
var barca = {"type":"team","description":"Bad","trophies":[{"ucl":"3"}]};
var rusDiff = require('rus-diff').rusDiff
console.log(rusDiff(madrid, barca))

Outputs:

{ '$unset': { 'trophies.1': true },
  '$set': { description: 'Bad', 'trophies.0.ucl': '3' } }
Mirek Rusin
  • 18,820
  • 3
  • 43
  • 36
2

One more modification to Gabriel Gartz's version. This one shows both values before/after for each key in a tuple [oldValue, newValue] and doesn't include empty branches in the diff

function diffObj( obj1, obj2 ) {

    const result = {};
    if ( Object.is( obj1, obj2 ) ) return undefined;
    if ( ! obj2 || typeof obj2 !== 'object' ) return obj2;
    Object.keys( obj1 || {} ).concat( Object.keys( obj2 || {} ) ).forEach( key => {

        let val;
        if ( obj2[ key ] !== obj1[ key ] && ! Object.is( obj1[ key ], obj2[ key ] ) ) val = [ obj1[ key ], obj2[ key ] ];

        if ( typeof obj2[ key ] === 'object' && typeof obj1[ key ] === 'object' ) {

            const value = diffObj( obj1[ key ], obj2[ key ] );
            if ( value !== undefined ) val = value;

        } else if ( val !== undefined ) {

            result[ key ] = val;

        }

    } );
    return result;

}
Pawel
  • 16,093
  • 5
  • 70
  • 73
1

For me this worked:

const a ={
    'kk':0,
    'k1':1,
    'k2':2,
    'k4':4, 
    'sub': {
        'xx':0,
        'x1':1,
        'x2':2,
        'x4':4, 
    } 
};
const b ={
    'kk':0,
    'k1':1,
    'k3':3, 
    'k4':44, 
    'sub': {
        'xx':0,
        'x1':1,
        'x3':3, 
        'x4':44, 
    }
};


function ObjectsDiff(o1,o2) {
    const typeObject = function(o){
        return typeof o === 'object';
    };
    const diff = function (o1, o2) {
        const result = {};
        // if first is item is not object
        if (!typeObject(o1) && typeObject(o2)) {
            return o2;
        }
        // if second is item is not object
        else if (typeObject(o1) && !typeObject(o2)) {
            return undefined;
        }
        // if they are equal
        else if (Object.is(o1, o2)) {
            return undefined;
        }
        const keys = Object.keys(o2);
        for (let i=0; i<keys.length; i++) {
            const key = keys[i];
            // if both are objects
            if ( typeObject(o1[key]) && typeObject(o2[key])) {
                // if equal, return nothing
                if ( Object.is(o1[key], o2[key]) ) {
                    // do nothing
                } else if (o1[key] === o2[key]) {
                    // do nothing
                } else {
                    result[key] = diff(o1[key],o2[key]);
                }
            } else if (o1[key] !== o2[key]) {
                result[key] = o2[key];
            } else {
                // do nothing
            }
        }
        return result;
    };
    return [
        diff(o1,o2),
        diff(o2,o1),
    ];
}

console.log( ObjectsDiff(a,b));
T.Todua
  • 53,146
  • 19
  • 236
  • 237
0

There are these three scenarios:

  1. A property exists on both the objects.

    • If the value of the property is same in both the objects, then delete it from the second object (copy).
  2. A property exists on the first object (original), but not on the second object. In this case add the property to the second object.

  3. And finally a property exists on the second object but not on the first, in this case, do nothing.

    function compare(original, copy) {
      for (let [k, v] of Object.entries(original)) {
        if (typeof v === "object" && v !== null) {
          if (!copy.hasOwnProperty(k)) {
            copy[k] = v; // 2
          } else {
            compare(v, copy?.[k]);
          }
        } else {
          if (Object.is(v, copy?.[k])) {
            delete copy?.[k]; // 1
          }
        }
      }
      return JSON.stringify(copy);
    }
    
    const 
      madrid =
        '{"type":"team","description":"Good","trophies":[{"ucl":"10"}, {"copa":"5"}]}',
      barca = '{"type":"team","description":"Bad","trophies":[{"ucl":"3"}]}',
      diff = compare(JSON.parse(madrid), JSON.parse(barca));
    
    console.log(diff);
SSM
  • 2,855
  • 1
  • 3
  • 10
0

I had the same challenge and created a light API to do this as I found the others too complex -- get diff between objects, and ability to merge a diff back into json data. Looks like it does exactly what you need:

The diff object uses tags like __deleted or __mergeAction to keep track what the change is for an object such as a deleted array record, or a new / updated record in an array. Otherwise non-object keys that change (such as the description field in your example) just show up in the delta.

// Get the delta between two json objects in a format that can be used by the mergeJson function
var edits = JsonDiffMerge.jsonDiff(originalData, updatedData);

// Merge the edits into the original object. The original object is not modified. The result is returned.
var merged = JsonDiffMerge.mergeJson(true, originalData, edits);
Shane
  • 303
  • 2
  • 9
-1

function diff(obj1, obj2) {
    const result = {};
    if (Object.is(obj1, obj2)) {
        return undefined;
    }
    if (!obj2 || typeof obj2 !== 'object') {
        return obj2;
    }
    Object.keys(obj1 || {}).concat(Object.keys(obj2 || {})).forEach(key => {
        if(obj2[key] !== obj1[key] && !Object.is(obj1[key], obj2[key])) {
            result[key] = obj2[key];
        }
        if(typeof obj2[key] === 'object' && typeof obj1[key] === 'object') {
            const value = diff(obj1[key], obj2[key]);
            if (value !== undefined) {
                result[key] = value;
            }
        }
    });
    return result;
}
user13512145
  • 45
  • 1
  • 5