Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Deep diff between two object, using lodash
/**
* 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);
}
@Aidurber

This comment has been minimized.

Copy link

commented May 2, 2017

Exactly what I was looking for, thanks for this @Yimiprod.

@Odywan55

This comment has been minimized.

Copy link

commented Jun 27, 2017

Cool, thank you!

@weisk

This comment has been minimized.

Copy link

commented Nov 14, 2017

good stuff

@Linux249

This comment has been minimized.

Copy link

commented Nov 16, 2017

thanks

@oleg-nazarov

This comment has been minimized.

Copy link

commented Nov 29, 2017

best practice for me

@claudioc

This comment has been minimized.

Copy link

commented Dec 7, 2017

Thanks!

@jeongsd

This comment has been minimized.

Copy link

commented Dec 11, 2017

Good

@kaanon

This comment has been minimized.

Copy link

commented Dec 18, 2017

This works well. Thank you!

@retyui

This comment has been minimized.

Copy link

commented Jan 18, 2018

Functional programming (FP) exmaple: 🥇

import { transform, isEqual, isObject } from "lodash/fp";

const _transform = transform.convert({
	cap: false
});

const iteratee = baseObj => (result, value, key) => {
	if (!isEqual(value, baseObj[key])) {
		const valIsObj = isObject(value) && isObject(baseObj[key]);
		result[key] = valIsObj === true ? differenceObject(value, baseObj[key]) : value;
	}
};

export function differenceObject(targetObj, baseObj) {
	return _transform(iteratee(baseObj), null, targetObj);
}
@pivcec

This comment has been minimized.

Copy link

commented Jan 18, 2018

thanks!

@codeofsumit

This comment has been minimized.

Copy link

commented Jan 21, 2018

sweeeet thanks!

@rspicer

This comment has been minimized.

Copy link

commented Feb 13, 2018

Can't confirm if this works on 4.x, but definitely works on 3.x. Thanks so much man!

@borantula

This comment has been minimized.

Copy link

commented Feb 13, 2018

both nice examples thanks :D

@nickthakkar

This comment has been minimized.

Copy link

commented Feb 21, 2018

Awesome @Yimiprod.! This is what i was looking for. Thanks a lot !

@Seetha93

This comment has been minimized.

Copy link

commented Feb 24, 2018

Awesome! Thanks

@dominiceden

This comment has been minimized.

Copy link

commented Mar 9, 2018

Thanks, very helpful!

@Thithi32

This comment has been minimized.

Copy link

commented Mar 28, 2018

Thanks. I tried to make an underscore version shared here: https://gist.github.com/Thithi32/d0a4fcd85953b475a39ef4709e3e7ef5

@jvanderberg

This comment has been minimized.

Copy link

commented Apr 22, 2018

Thanks much, this works well.

A bit more concisely, not sure the reason for the nested function, as nothing is closed over.

import { transform, isEqual, isObject } from 'lodash';

/**
 * 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) {
	return transform(object, (result, value, key) => {
		if (!isEqual(value, base[key])) {
			result[key] = isObject(value) && isObject(base[key]) ? difference(value, base[key]) : value;
		}
	});
}
@isaacw

This comment has been minimized.

Copy link

commented Apr 24, 2018

Lifesaver!!!

@racingrebel

This comment has been minimized.

Copy link

commented May 9, 2018

Thank you, very nice and clean!
I agree with you @jvanderberg.

@jorgeperegrina

This comment has been minimized.

Copy link

commented May 11, 2018

Googled what I wanted to do, clicked on first result, and the magic happened... Thanks a lot!!!

@vitia

This comment has been minimized.

Copy link

commented Jun 8, 2018

Diff object from these do not reflect removed fields.

@ioanniswd

This comment has been minimized.

Copy link

commented Jun 10, 2018

awesome

@devp619

This comment has been minimized.

Copy link

commented Jun 13, 2018

This will not work for arrays located within the object 😞

{
  "l0s": [
    {
      "name": "Level 0 Name1",
      "l1s": [
        {
          "name": "Level 1 Name1",
          "l2s": [
            {
              "name": "Level 2 Name1"
            },
            {
              "name": "Level 2 Name2"
            }
          ]
        },
        {
          "name": "Level 1 Name2",
          "l2s": [
            {
              "name": "Level 2 Name3"
            },
            {
              "name": "Level 2 Name4"
            }
          ]
        }
      ]
    },
    {
      "name": "Level 0 Name2",
      "l1s": [
        {
          "name": "Level 1 Name3",
          "l2s": [
            {
              "name": "Level 2 Name5"
            },
            {
              "name": "Level 2 Name6"
            }
          ]
        },
        {
          "name": "Level 1 Name4",
          "l2s": [
            {
              "name": "Level 2 Name7"
            }
          ]
        }
      ]
    }
  ]
}
@darlandieterich

This comment has been minimized.

Copy link

commented Jul 12, 2018

easter egg 0/

@denis-shostik

This comment has been minimized.

Copy link

commented Jul 13, 2018

In my case it doesn't work properly, you can test comparison with this objects

 obj1 = {
    b: 2, 
    c: ['2', '1'], 
    d: { baz: 1, bat: 2 },
    e: 1
}

obj2 = {
    a: 1,
    b: 2,
    c: ['1', '2'],
    d: { baz: 1, bat: 2 }
}

difference(obj1, obj2)

And in result we get this following, that actually is wrong, because arrays have the same values

{
    a: 1
    c: ["1", "2"]
}

It can be fixed if we use for arrays comparing with initial sorting, but the array can be deep in object and I don't know how to apply it in code that above, can be somebody have any propositions about it?

_.isEqual(array1.sort(), array2.sort()); 

I have solution for this case, but it looks ugly https://pastebin.com/AtRLqWUr
Oh, I found better solution, with method isEqualWith, https://pastebin.com/M43T4v0k

@code-hunger

This comment has been minimized.

Copy link

commented Jul 16, 2018

@denis-shostik no, it does work properly, [1,2] and [2,1] are two different arrays.

@fourthgenz28

This comment has been minimized.

Copy link

commented Aug 28, 2018

Very useful, thank you for sharing!

@BoffoF

This comment has been minimized.

Copy link

commented Sep 6, 2018

Found a fix to suppress null values in diffed arrays,
e.g.
base = [1,2,3]
object = [1,2,4,5]

result (intial code): [null, null, 4,5]
result (my code): [4,5]

function difference(object, base) {
   function changes(object, base) {
        let arrayIndexCounter = 0;
        return _.transform(object, function (result, value, key) {
            if (!_.isEqual(value, base[key])) {
                let resultKey = _.isArray(base) ? arrayIndexCounter++ : key;
                result[resultKey] = (_.isObject(value) && _.isObject(base[key])) ? changes(value, base[key]) : value;
                console.log("Result: " + JSON.stringify(result));
            }
        });
    }
    return changes(object, base);
}
@cfator

This comment has been minimized.

Copy link

commented Sep 11, 2018

What about JSON.stringify(obj1) === JSON.stringify(obj2)?

@stubar

This comment has been minimized.

Copy link

commented Sep 27, 2018

What about JSON.stringify(obj1) === JSON.stringify(obj2)?

That only tells you if it's different, the original functions will "Return a new object who represent the dif"

@Naveenkariyappa

This comment has been minimized.

Copy link

commented Oct 10, 2018

Tried to used a function
e.g.
base = [1,2,3]
object = [1,2,4,5]

difference(base,object){
// its says _.transform not defined
}

info i have impported _lodash in my file,
advance appologies , Need help i am new to this

@lelandsmith

This comment has been minimized.

Copy link

commented Oct 24, 2018

Tried to used a function
e.g.
base = [1,2,3]
object = [1,2,4,5]

difference(base,object){
// its says _.transform not defined
}

info i have impported _lodash in my file,
advance appologies , Need help i am new to this

@Naveenkariyappa I have also been getting the same error, did you figure out how to fix this?

@BinarySpike

This comment has been minimized.

Copy link

commented Nov 5, 2018

@Naveenkariyappa and @lelandsmith,
If using node:
var _ = require('lodash')

See https://lodash.com/

@hieudang-agilityio

This comment has been minimized.

Copy link

commented Nov 26, 2018

Thanks

@nosovicki

This comment has been minimized.

Copy link

commented Dec 11, 2018

In my case it doesn't work properly, you can test comparison with this objects

It can be fixed if we use for arrays comparing with initial sorting, but the array can be deep in object and I don't know how to apply it in code that above, can be somebody have any propositions about it?

_.isEqual(array1.sort(), array2.sort()); 

No need to complicate the diff function. You need it if you use your array as a set, but there's no way to tell it. Better sort your sets within your objects before comparison.

@xibre

This comment has been minimized.

Copy link

commented Dec 12, 2018

The original function throws a TypeError if base is null or undefined:

difference({ a: true }, null);      // -> TypeError: Cannot read property 'a' of null
difference({ a: true }, undefined); // -> TypeError: Cannot read property 'a' of undefined

In these cases I'd expect it to return object so I have added a simple check and it works fine for me now:

function difference(object, base) {
  if (!base) return object;
  ...
@nirpeled

This comment has been minimized.

Copy link

commented Feb 1, 2019

Pure gold!!

@UltraKenchie

This comment has been minimized.

Copy link

commented Mar 26, 2019

Thanks much, this works well.

A bit more concisely, not sure the reason for the nested function, as nothing is closed over.

import { transform, isEqual, isObject } from 'lodash';

/**
 * 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) {
	return transform(object, (result, value, key) => {
		if (!isEqual(value, base[key])) {
			result[key] = isObject(value) && isObject(base[key]) ? difference(value, base[key]) : value;
		}
	});
}

Loved this. But anyone experienced this code leaving an empty object property?

let source = {
 name: 'test',
 add: 'no'
 link: {
  a: 'fb',
  b: 'twt'
 }
}

let next = {
 name: 'change,
 link: {
  a: 'fb',
  b: 'twt'
 }
}

difference(next, source);

this results in

{
 name: 'change',
 link: {}
}

Is that normal?
I've fixed it by deleting the property if is empty

result[key] = isObject(value) && isObject(base[key]) ? this.difference(value, base[key]) : value;
if (isObject(result[key]) && isEmpty(result[key])) {
     delete result[key];
} else if (isEmpty(result[key])) {
     result[key] = null;
}
@ngothanhtai

This comment has been minimized.

Copy link

commented Apr 14, 2019

really helpful, it helps me to check the different between props and nextProps in React app. Thank you!

@Turbiani

This comment has been minimized.

Copy link

commented May 2, 2019

@Yimiprod thanks for that. Is really helpful! In my case, the object base come from MongoDB, so I had add some changes at your original code. If somebody has same case, this is my code.

const _ = require('lodash');
const mongoose = require('mongoose');

function isObjectId(data) {
  if (mongoose.Types.ObjectId.isValid(data) && typeof data === 'object') {
    if (Object.prototype.hasOwnProperty.call(data, 'id') && (typeof data.id === 'object')) {
      return true;
    }
  }
  return false;
}

function customizer(objValue, othValue) {
  if (isObjectId(objValue) && isObjectId(othValue)) {
    return objValue.id.toString('hex') === othValue.id.toString('hex');
  }
  if (isObjectId(objValue) && !isObjectId(othValue)) {
    return objValue.id.toString('hex') === othValue;
  }
  if (!isObjectId(objValue) && isObjectId(othValue)) {
    return objValue === othValue.id.toString('hex');
  }
  return undefined;
}

function difference(object, base) {
  // eslint-disable-next-line no-shadow
  function changes(object, base) {
    return _.transform(object, (result, value, key) => {
      if (!_.isEqualWith(value, base[key], customizer)) {
        result[key]
          = (_.isObject(value) && _.isObject(base[key])) ? changes(value, base[key]) : value;
      }
    });
  }
  return changes(object, base);
}
@NicolasLetellier

This comment has been minimized.

Copy link

commented May 30, 2019

This work well when the difference is an added property to the object compared to the base, but not when a property is removed from that base:
As the iteratee function of _.transform is based on the object keys, it can't retrieve keys of the base that have been removed in between.

@UltraKenchie , that's may be why you get an empty object, in my case it was when the object is different from the base, but only by a removed property (in this case, the returned result object from the iteratee stay empty). When a property is removed along other properties' changes, the removal is not identified neither.

Just for you to be sure for what you want to use it.

@dannymartinm

This comment has been minimized.

Copy link

commented Jun 11, 2019

Works like charm! Thanks for sharing.

@Yimiprod

This comment has been minimized.

Copy link
Owner Author

commented Jun 12, 2019

Woah i didn't know that gist was still here, thanks everyone for your comments.
I really like the version @jvanderberg proposed.
The FP version from @retyui is really nice too.

@AterDeus

This comment has been minimized.

Copy link

commented Oct 9, 2019

@NicolasLetellier Can you help with code, which shows when property is removed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.