Skip to content

Instantly share code, notes, and snippets.

@Yimiprod
Last active June 20, 2024 08:26
Show Gist options
  • Save Yimiprod/7ee176597fef230d1451 to your computer and use it in GitHub Desktop.
Save Yimiprod/7ee176597fef230d1451 to your computer and use it in GitHub Desktop.
Deep diff between two object, using lodash
/**
* This code is licensed under the terms of the MIT license
*
* Deep diff between two object, using lodash
* @param {Object} object Object compared
* @param {Object} base Object to compare with
* @return {Object} Return a new object who represent the diff
*/
function difference(object, base) {
function changes(object, base) {
return _.transform(object, function(result, value, key) {
if (!_.isEqual(value, base[key])) {
result[key] = (_.isObject(value) && _.isObject(base[key])) ? changes(value, base[key]) : value;
}
});
}
return changes(object, base);
}
@ernistkg
Copy link

This is my vanilla way, i hope it help somebody.

/**
 * Recursive diff between two object
 * @param  {Object} base   Object to compare with
 * @param  {Object} object Object compared
 * @return {Object} Return a new object who represent the diff OR Return FALSE if there is no difference
 */
function differenceBetweenObjects (base,object){
    let diff = false;
    if(typeof object == 'object'){
        for(let k in object){
            if(typeof object[k] != 'object'){
                if(base[k] != object[k]){
                    if(diff == false){diff = {};}
                    diff[k] = object[k];
                }
            }
            else{
                let subDiff = differenceBetweenObjects(base[k],object[k]);
                if(subDiff !== false){
                    if(diff == false){diff = {};}
                    diff[k] = subDiff;
                }
            }
        }
    }
    return diff;
}

Doesn't work with this:

differenceBetweenObjects({a: 2, b: [1,2,3,4], c: {r: 34534}}, {a: 1, b: [1,2,3,4,5]})

@nemethadam
Copy link

nemethadam commented May 29, 2021

This is my vanilla way, i hope it help somebody.

/**
 * Recursive diff between two object
 * @param  {Object} base   Object to compare with
 * @param  {Object} object Object compared
 * @return {Object} Return a new object who represent the diff OR Return FALSE if there is no difference
 */
function differenceBetweenObjects (base,object){
    let diff = false;
    if(typeof object == 'object'){
        for(let k in object){
            if(typeof object[k] != 'object'){
                if(base[k] != object[k]){
                    if(diff == false){diff = {};}
                    diff[k] = object[k];
                }
            }
            else{
                let subDiff = differenceBetweenObjects(base[k],object[k]);
                if(subDiff !== false){
                    if(diff == false){diff = {};}
                    diff[k] = subDiff;
                }
            }
        }
    }
    return diff;
}

Doesn't work with this:

differenceBetweenObjects({a: 2, b: [1,2,3,4], c: {r: 34534}}, {a: 1, b: [1,2,3,4,5]})

I forgot detecting missing keys from base object. Try now. Thanks the report.

/**
 * Deep diff between two object, using lodash
 * @param  {Object} base   Object to compare with
 * @param  {Object} object Object compared
 * @return {Object} Return a new object who represent the diff OR Return FALSE if there is no difference
 */
function differenceBetweenObjects (base,object){
    let diff = false;
    if(typeof object == 'object'){
        for(let k in object){
            if(typeof object[k] != 'object'){
                if(base[k] != object[k]){
                    if(diff == false){diff = {};}
                    diff[k] = object[k];
                }
            }
            else{
                let subDiff = differenceBetweenObjects(base[k],object[k]);
                if(subDiff !== false){
                    if(diff == false){diff = {};}
                    diff[k] = subDiff;
                }
            }
        }
    }
    if(typeof base == 'object'){
        for(let k in base){
            if(!object.hasOwnProperty(k)){
                diff[k] = null;
            }
        }
    }
    return diff;
}

@nemethadam
Copy link

nemethadam commented May 29, 2021

I am thinking. Should I check missing keys recusive if base object's item is an object? Like this:

function differenceBetweenObjects (base,object){
    let diff = false;
    if(typeof object == 'object' && typeof base == 'object'){
        for(let k in object){
            if(typeof object[k] != 'object'){
                if(base[k] != object[k]){
                    if(diff == false){diff = {};}
                    diff[k] = object[k];
                }
            }
            else{
                let subDiff = differenceBetweenObjects(base[k],object[k]);
                if(subDiff !== false){
                    if(diff == false){diff = {};}
                    diff[k] = subDiff;
                }
            }
        }
    }
    if(typeof base == 'object'){
        for(let k in base){
            if(typeof object != 'object' || !object.hasOwnProperty(k)){
                if(diff == false){diff = {};}
                if(typeof base[k] == 'object'){
                    let subDiff = differenceBetweenObjects(base[k]);
                    diff[k] = subDiff;
                }
                else{
                    diff[k] = null;
                }
            }
        }
    }
    return diff;
}

@Avivhdr
Copy link

Avivhdr commented Aug 19, 2021

@sivaraj-v
Copy link

image

I'm the 500th person to save this snippet:-)

@Shereef
Copy link

Shereef commented Oct 12, 2021

Thank you, very nice and clean!
I agree with you @jvanderberg and I used it for typescipt!

@tleguede
Copy link

thanks , it helps a lot

@anicioalexandre
Copy link

Awesome code! I did a small change to deal with an array of objects as a key of the object we wanted to diff:

export const deepDiffBetweenObjects = (object, base) => {
  const changes = (object, base) => {
    return transform(object, (result, value, key) => {
      if (!isEqual(value, base[key])) {
        if (isArray(value)) {
          result[key] = difference(value, base[key])
        } else if (isObject(value) && isObject(base[key])) {
          result[key] = changes(value, base[key])
        } else {
          result[key] = value
        }
      }
    })
  }
  return changes(object, base)
}

@Chippd
Copy link

Chippd commented Jan 1, 2022

@chtseac Yours was the best solution I've found so far, thank you from Dublin!

@Chippd
Copy link

Chippd commented Jan 2, 2022

Sharing my own update - I needed to only compare some top-level properties of two objects, so added an optional third parameter to take an array of paths to specifically check.

let changes = deepDiff(previousValue, newValue, [
    "customisations",
    "customisations.localeOverrides", // won't work currently - someone smarter can me can try make it work :) 
    "onboarding"
  ]);

function deepDiff(fromObject, toObject, specificPaths) {
  const changes = {};
  console.log('specificPaths:', specificPaths);
  
  const buildPath = (path, obj, key) =>
    _.isUndefined(path) ? key : `${path}.${key}`;

  let obj1 = {}; obj2 = {}
  if(specificPaths && specificPaths.length > 0){
    // only look at specific paths if specified
    for (const path of specificPaths) {
      if(fromObject[path]) obj1[path] = fromObject[path];
      if(toObject[path]) obj2[path] = toObject[path]
    }
  } else {
    obj1 = fromObject
    obj2 = toObject
  }

  
  const walk = (fromObject, toObject, path) => {
    for (const key of _.keys(fromObject)) {
      const currentPath = buildPath(path, fromObject, key);
      if (!_.has(toObject, key)) {
        changes[currentPath] = { from: _.get(fromObject, key) };
      }
    }

    for (const [key, to] of _.entries(toObject)) {
      const currentPath = buildPath(path, toObject, key);
      if (!_.has(fromObject, key)) {
        changes[currentPath] = { to };
      } else {
        const from = _.get(fromObject, key);
        if (!_.isEqual(from, to)) {
          if (_.isObjectLike(to) && _.isObjectLike(from)) {
            walk(from, to, currentPath);
          } else {
            changes[currentPath] = { from, to };
          }
        }
      }
    }
  };

  walk(obj1, obj2); 

  return changes;
}

@stellarArg
Copy link

Here is an update of @Chippd with specifics paths
`
function deepDiff(fromObject, toObject, specificPaths) {
const changes = {};
console.log('specificPaths:', specificPaths);

const buildPath = (path, __, key) =>
    _.isUndefined(path) ? key : `${path}.${key}`;

let obj1 = {}; 
let obj2 = {}
if (_.isArray(specificPaths) && !_.isEmpty(specificPaths)) {
    for (const path of specificPaths) {
        if (_.has(fromObject, path)) {
            _.set(obj1, path, _.get(fromObject, path));
        } else if (_.has(toObject, path)) {
            changes[path] = {to: _.get(toObject, path)};
        }
        if (_.has(toObject, path)) {
            _.set(obj2, path, _.get(toObject, path));
        } else if (_.has(fromObject, path)) {
            changes[path] = {from: _.get(fromObject, path)};
        }
    }
} else {
    obj1 = fromObject
    obj2 = toObject
}


const walk = (fromObject, toObject, path) => {
    for (const key of _.keys(fromObject)) {
        const currentPath = buildPath(path, fromObject, key);
        if (!_.has(toObject, key)) {
            changes[currentPath] = { from: _.get(fromObject, key) };
        }
    }

    for (const [key, to] of _.entries(toObject)) {
        const currentPath = buildPath(path, toObject, key);
        if (!_.has(fromObject, key)) {
            changes[currentPath] = { to };
        } else {
            const from = _.get(fromObject, key);
            if (!_.isEqual(from, to)) {
                if (_.isObjectLike(to) && _.isObjectLike(from)) {
                    walk(from, to, currentPath);
                } else {
                    changes[currentPath] = { from, to };
                }
            }
        }
    }
};

walk(obj1, obj2);

return changes;

}

const previousValue = {
customisations: {
localeOverrides: {
foo: 1,
},
bar: 2
},
bar: [1,2,3],
onboarding: {
foo: 1
},
foo: 1
}

const newValue = {
customisations: {
localeOverrides: {
daz: 1,
},
bar: 2,
daz: 2
},
onboarding: {
foo: 4
},
baz: 2
}

const changes = deepDiff(previousValue, newValue, [
"customisations",
"customisations.localeOverrides",
"onboarding"
]);

// Only specific path
const changes2 = deepDiff(previousValue, newValue, [
"customisations.localeOverrides",
"bar"
]);

// test only if validate that array is present and isn't empty
const changes3 = deepDiff(previousValue, newValue, []);

// no array present
const changes4 = deepDiff(previousValue, newValue);

console.log('compare result various paths', changes);
console.log('compare result Only specific path', changes2);
console.log('compare result test only if validate that array is present and isn't empty', changes3);
console.log('compare result no array present', changes4);

`

@mygod48
Copy link

mygod48 commented Aug 15, 2022

Just one example works fine in my case (shallow diff):

const diffData = _.fromPairs( _.differenceWith(_.toPairs(sourceData), _.toPairs(valuesToDiffWith), _.isEqual), )

any suggestions for deep diff?

@jotasenator
Copy link

thank you, it helps me and makes me look prop :). lodash is superbe

@magicsd
Copy link

magicsd commented Oct 11, 2022

Just another shallow diff

const shallowDiff = Object.entries(object1).reduce(
  (diff, [key, value]) =>
    _isEqual(object2[key], value) ? diff : { ...diff, [key]: value },
  {},
)

@Andrei-Fogoros
Copy link

Hello @Yimiprod,

Under which license is the above code released?

Thanks in advance,
Andrei

@Yimiprod
Copy link
Author

@Andrei-Fogoros didn't thinked about licence at the time i posted it, but since you're asking, it's a good occasion to put it under MIT License.

@Andrei-Fogoros
Copy link

@Andrei-Fogoros didn't thinked about licence at the time i posted it, but since you're asking, it's a good occasion to put it under MIT License.

Great, thank you very much! :)

@hyunkayhan
Copy link

Not sure if anyone needs this variation, but here's one I made to basically create a separate JSON object with ONLY the changes with accurate child items.

import * as _ from 'lodash';

/**
 * Deep diff between two object-likes
 * @param  {Object} fromObject the original object
 * @param  {Object} toObject   the updated object
 * @return {Object}            a new object which represents the diff
 */
export function deepDiff(fromObject, toObject) {
    const changes = {};

    const buildPath = (path, obj, key) => {
        const origVal = _.get(obj, key);
        if (_.isUndefined(path)) {
            if (_.isArray(origVal)) {
                changes[key] = [];
            } else if (_.isObject(origVal)) {
                changes[key] = {};
            }
        } else {
            if (_.isArray(origVal)) {
                path[key] = [];
            } else if (_.isObject(origVal)) {
                path[key] = {};
            }
        }
        return [_.isUndefined(path) ? changes : path, key]
    }
        

    const walk = (fromObject, toObject, path) => {
        for (const key of _.keys(fromObject)) {
            const objKeyPair = buildPath(path, fromObject, key);
            if (!_.has(toObject, key)) {
                objKeyPair[0][objKeyPair[1]] = { from: _.get(fromObject, key) };
            }
        }

        for (const [key, to] of _.entries(toObject)) {
            const isLast = _.has(fromObject, key);
            const objKeyPair = buildPath(path, fromObject, key);
            if (isLast) {
                const from = _.get(fromObject, key);
                if (!_.isEqual(from, to)) {
                    if (_.isObjectLike(to) && _.isObjectLike(from)) {
                        walk(from, to, objKeyPair[0][objKeyPair[1]]);
                    } else {
                        objKeyPair[0][objKeyPair[1]] = { __old: from, __new: to };
                    }
                } else {
                    delete objKeyPair[0][objKeyPair[1]]
                }
            } else {
                objKeyPair[0][objKeyPair[1]] = { to };
            }
        }
    };

    walk(fromObject, toObject);

    return changes;
}

@jfortez
Copy link

jfortez commented Mar 1, 2024

ty

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment