78

I am using node, mocha, and chai for my application. I want to test that my returned results data property is the same "type of object" as one of my model objects (Very similar to chai's instance). I just want to confirm that the two objects have the same sets of property names. I am specifically not interested in the actual values of the properties.

Let's say I have the model Person like below. I want to check that my results.data has all the same properties as the expected model does. So in this case, Person which has a firstName and lastName.

So if results.data.lastName and results.data.firstName both exist, then it should return true. If either one doesn't exist, it should return false. A bonus would be if results.data has any additional properties like results.data.surname, then it would return false because surname doesn't exist in Person.

This model

function Person(data) {
  var self = this;
  self.firstName = "unknown";
  self.lastName = "unknown";

  if (typeof data != "undefined") {
     self.firstName = data.firstName;
     self.lastName = data.lastName;
  }
}
Chaurasia
  • 494
  • 1
  • 6
  • 22
dan27
  • 1,425
  • 3
  • 14
  • 13

9 Answers9

131

You can serialize simple data to check for equality:

data1 = {firstName: 'John', lastName: 'Smith'};
data2 = {firstName: 'Jane', lastName: 'Smith'};
JSON.stringify(data1) === JSON.stringify(data2)

This will give you something like

'{firstName:"John",lastName:"Smith"}' === '{firstName:"Jane",lastName:"Smith"}'

As a function...

function compare(a, b) {
  return JSON.stringify(a) === JSON.stringify(b);
}
compare(data1, data2);

EDIT

If you're using chai like you say, check out http://chaijs.com/api/bdd/#equal-section

EDIT 2

If you just want to check keys...

function compareKeys(a, b) {
  var aKeys = Object.keys(a).sort();
  var bKeys = Object.keys(b).sort();
  return JSON.stringify(aKeys) === JSON.stringify(bKeys);
}

should do it.

Casey Foster
  • 5,982
  • 2
  • 27
  • 27
  • 13
    I do not want to check the actual values of the properties, just the property names. sorry for the confusion – dan27 Jan 16 '13 at 23:05
  • that is exactly what i was looking for...new to JS and wasn't sure how to do the property reflection. Thanks! – dan27 Jan 17 '13 at 01:24
  • 38
    + 1 for idea, but watch out for trap - **order of arguments is important** in your method: `JSON.stringify({b:1, a:1})` **differs from** `JSON.stringify({a:1, b:1})` – fider Jun 20 '13 at 15:30
  • 5
    It works today because most browsers maintain some kind of ordering for an object keys but the ecma spec doesn't require it, so this code could fail. – AlexG Oct 19 '14 at 17:16
  • +AlexG It works now so I guess that's enough more most people -_- – rfcoder89 Jan 18 '16 at 15:35
  • chai has the ```keys()``` method for this: http://chaijs.com/api/bdd/#method_keys – Robin Elvin Dec 16 '16 at 14:56
  • 1
    if you need deep check / nested objects https://stackoverflow.com/questions/41802259/javascript-deep-check-objects-have-same-keys – RozzA Mar 01 '18 at 02:38
  • Please be aware that using JSON.stringify() for comparion will fail you (and result in an error thrown) if you have a circular reference in one of your objects. – Fannon Feb 20 '20 at 05:07
  • `JSON.stringify` also fails for objects with undefined values. Eg: `{ a: undefined }` and `{ b: undefined }` both return `"{}"` with `JSON.stringify()` – adiga Apr 24 '21 at 09:17
  • I tried this to compare two datasets of elements but it failed due to the order. Somehow the order got changed for a single element but all other 50-ish elements were fine. Spooky stuff. – Tim Vermaelen Jul 25 '22 at 12:07
55

2 Here a short ES6 variadic version:

function objectsHaveSameKeys(...objects) {
   const allKeys = objects.reduce((keys, object) => keys.concat(Object.keys(object)), []);
   const union = new Set(allKeys);
   return objects.every(object => union.size === Object.keys(object).length);
}

A little performance test (MacBook Pro - 2,8 GHz Intel Core i7, Node 5.5.0):

var x = {};
var y = {};

for (var i = 0; i < 5000000; ++i) {
    x[i] = i;
    y[i] = i;
}

Results:

objectsHaveSameKeys(x, y) // took  4996 milliseconds
compareKeys(x, y)               // took 14880 milliseconds
hasSameProps(x,y)               // after 10 minutes I stopped execution
schirrmacher
  • 2,341
  • 2
  • 27
  • 29
  • 1
    Awesome comparison! – Brandon Clark Jan 11 '17 at 21:03
  • 2
    Why did I get downvotes? Please write a comment so that I can improve my answer :) – schirrmacher Apr 13 '17 at 07:43
  • 1
    In order to return the number of different keys: `return objects.reduce((res, object) => res += union.size - Object.keys(object).length, 0);` – WaeCo Jun 23 '17 at 12:09
  • I tested the functions times and `hasSameProps` (**The Original, not the 2013.04.26 edit**) seems to be the fastest (AMD® Ryzen 5 3600x, node v18.4.0) `objectsHaveSameKeys: 3.275s compareKeys: 2.174s hasSameProps: 723.42ms` – Miguel Dec 02 '22 at 22:25
19

If you want to check if both objects have the same properties name, you can do this:

function hasSameProps( obj1, obj2 ) {
  return Object.keys( obj1 ).every( function( prop ) {
    return obj2.hasOwnProperty( prop );
  });
}

var obj1 = { prop1: 'hello', prop2: 'world', prop3: [1,2,3,4,5] },
    obj2 = { prop1: 'hello', prop2: 'world', prop3: [1,2,3,4,5] };

console.log(hasSameProps(obj1, obj2));

In this way you are sure to check only iterable and accessible properties of both the objects.

EDIT - 2013.04.26:

The previous function can be rewritten in the following way:

function hasSameProps( obj1, obj2 ) {
    var obj1Props = Object.keys( obj1 ),
        obj2Props = Object.keys( obj2 );

    if ( obj1Props.length == obj2Props.length ) {
        return obj1Props.every( function( prop ) {
          return obj2Props.indexOf( prop ) >= 0;
        });
    }

    return false;
}

In this way we check that both the objects have the same number of properties (otherwise the objects haven't the same properties, and we must return a logical false) then, if the number matches, we go to check if they have the same properties.

Bonus

A possible enhancement could be to introduce also a type checking to enforce the match on every property.

Ragnarokkr
  • 2,328
  • 2
  • 21
  • 31
  • I think this will work too. Very similar to Casey's. Thanks – dan27 Jan 17 '13 at 01:24
  • 1
    Doesn't this only check of `obj2` has `obj1`'s properties, and not vice versa? – Arithmomaniac Apr 26 '13 at 19:57
  • 2
    This function checks whether all the properties of `obj1` are present in `obj2`, so they have the same properties. But not vice versa. If you wanna skip the iterations on objects with different number of properties, then have to add a check on the number of properties in both the objects, and return a logical false in case they don't match. – Ragnarokkr Apr 26 '13 at 21:57
  • It looks like it is checking only the first level properties, right? – Paranoid Android Nov 21 '17 at 14:25
  • @Mirko yes. Please note that the check is done by looking for same keys into the object. It's not based on their effective values. (So for example, i could have two `name` keys assigned one to a string and one to a number, and the check would still return truthiness). However you could adapt it by implementing some sort of recorsivity in case of object keys, but it will require to extend the check for the data types. – Ragnarokkr Nov 21 '17 at 16:37
  • .every skips the last element, instead of .every use .forEach – Atul Bhosale Oct 23 '19 at 15:26
10

If you want deep validation like @speculees, here's an answer using deep-keys (disclosure: I'm sort of a maintainer of this small package)

// obj1 should have all of obj2's properties
var deepKeys = require('deep-keys');
var _ = require('underscore');
assert(0 === _.difference(deepKeys(obj2), deepKeys(obj1)).length);

// obj1 should have exactly obj2's properties
var deepKeys = require('deep-keys');
var _ = require('lodash');
assert(0 === _.xor(deepKeys(obj2), deepKeys(obj1)).length);

or with chai:

var expect = require('chai').expect;
var deepKeys = require('deep-keys');
// obj1 should have all of obj2's properties
expect(deepKeys(obj1)).to.include.members(deepKeys(obj2));
// obj1 should have exactly obj2's properties
expect(deepKeys(obj1)).to.have.members(deepKeys(obj2));
Philip Garrison
  • 576
  • 3
  • 8
5

Here's a deep-check version of the function provided above by schirrmacher. Below is my attempt. Please note:

  • Solution does not check for null and is not bullet proof
  • I haven't performance tested it. Maybe schirrmacher or OP can do that and share for the community.
  • I'm not a JS expert :).
function objectsHaveSameKeys(...objects) {
  const allKeys = objects.reduce((keys, object) => keys.concat(Object.keys(object)), [])
  const union = new Set(allKeys)
  if (union.size === 0) return true
  if (!objects.every((object) => union.size === Object.keys(object).length)) return false

  for (let key of union.keys()) {
    let res = objects.map((o) => (typeof o[key] === 'object' ? o[key] : {}))
    if (!objectsHaveSameKeys(...res)) return false
  }
  return true
}

Update 1

A 90% improvement on the recursive deep-check version is achieved on my computer by skipping the concat() and adding the keys directly to the Set(). The same optimization to the original single level version by schirrmacher also achieves ~40% improvement.

The optimized deep-check is now very similar in performance to the optimized single level version!

function objectsHaveSameKeysOptimized(...objects) {
  let union = new Set();
  union = objects.reduce((keys, object) => keys.add(Object.keys(object)), union);
  if (union.size === 0) return true
  if (!objects.every((object) => union.size === Object.keys(object).length)) return false

  for (let key of union.keys()) {
    let res = objects.map((o) => (typeof o[key] === 'object' ? o[key] : {}))
    if (!objectsHaveSameKeys(...res)) return false
  }
  return true
}

Performance Comparison

var x = {}
var y = {}
var a = {}
for (var j = 0; j < 10; ++j){
  a[j] = j
}

for (var i = 0; i < 500000; ++i) {
  x[i] = JSON.parse(JSON.stringify(a))
  y[i] = JSON.parse(JSON.stringify(a))
}

let startTs = new Date()
let result = objectsHaveSameKeys(x, y)
let endTs = new Date()
console.log('objectsHaveSameKeys = ' + (endTs - startTs)/1000)

Results

A: Recursive/deep-check versions*

  1. objectsHaveSameKeys = 5.185
  2. objectsHaveSameKeysOptimized = 0.415

B: Original non-deep versions

  1. objectsHaveSameKeysOriginalNonDeep = 0.517
  2. objectsHaveSameKeysOriginalNonDeepOptimized = 0.342
farqis
  • 61
  • 1
  • 3
  • I like this one, the only improvement I can see is checking for falsy before going recursively: `if (!res[0]) continue` before the `if (!objectsHaveSameKeys(...res)) return false` – Alberto Sadoc Oct 12 '21 at 10:06
  • @AlbertoSadoc, thanks for the suggestion! The condition `if(!res[0])` will never be true in the code as-is. However, if we `filter()` res, then it should work i.e. `res = res.filter((e) => (Object.keys(e).length !== 0))`. But the cost of filter() and Object.keys() is not justified since we do another Object.keys() on the recursive call anyways making most calls twice as expensive, just to save the cost on one exit scenario. And the extra code would not be worth it either. – farqis Nov 20 '21 at 03:42
1

Legacy Browser Object Compare Function

Unlike the other solutions posted here, my Object Compare Function works in ALL BROWSERS, modern or legacy, including very old browsers, even Internet Explorer 5 (c.2000)!

Features:

  1. Can compare an unlimited list of Objects. All must match or fails!
  2. Ignores property order
  3. Only compares "own" properties (i.e. non-prototype)
  4. Matches BOTH property names and property values (key-value pairs)!
  5. Matches functions signatures in objects!
  6. Every object submitted is cross-compared with each other to detect missing properties in cases where one is missing but not in the other
  7. Avoids null, undefined, NaN, Arrays, non-Objects, etc.
  8. {} empty object detection
  9. Works in almost ALL BROWSERS, including even Internet Explorer 5 and many other legacy browsers!
  • Note the function does not detect complex objects in properties, but you could rewrite the function to call them recursively.

Just call the method with as many objects as you like!

ObjectCompare(myObject1,myObject2,myObject3)
function ObjectCompare() {

    try {

        if (arguments && arguments.length > 0) {
            var len = arguments.length;
            if (len > 1) {
                var array = [];
                for (var i = 0; i < len; i++) {
                    if (
                        ((typeof arguments[i] !== 'undefined') || (typeof arguments[i] === 'undefined' && arguments[i] !== undefined))
                        && (arguments[i] !== null)
                        && !(arguments[i] instanceof Array)
                        && ((typeof arguments[i] === 'object') || (arguments[i] instanceof Object))
                    ) {
                        array.push(arguments[i]);
                    }
                }
                if (array.length > 1) {
                    var a1 = array.slice();
                    var a2 = array.slice();
                    var len1 = a1.length;
                    var len2 = a2.length;
                    var noKeys = true;
                    var allKeysMatch = true;
                    for (var x = 0; x < len1; x++) {
                        console.log('---------- Start Object Check ---------');
                        //if (len2>0) {
                        //  a2.shift();// remove next item
                        //}
                        len2 = a2.length;
                        if (len2 > 0 && allKeysMatch) {
                            for (var y = 0; y < len2; y++) {
                                if (x !== y) {// ignore objects checking themselves
                                    //console.log('Object1: ' + JSON.stringify(a1[x]));
                                    //console.log('Object2: ' + JSON.stringify(a2[y]));
                                    console.log('Object1: ' + a1[x].toString());
                                    console.log('Object2: ' + a2[y].toString());
                                    var ownKeyCount1 = 0;
                                    for (var key1 in a1[x]) {
                                        if (a1[x].hasOwnProperty(key1)) {
                                            // ---------- valid property to check ----------
                                            ownKeyCount1++;
                                            noKeys = false;
                                            allKeysMatch = false;// prove all keys match!
                                            var ownKeyCount2 = 0;
                                            for (var key2 in a2[y]) {
                                                if (a2[y].hasOwnProperty(key2) && !allKeysMatch) {
                                                    ownKeyCount2++;
                                                    if (key1 !== key1 && key2 !== key2) {// NaN check
                                                        allKeysMatch = true;// proven
                                                        break;
                                                    } else if (key1 === key2) {
                                                        if (a1[x][key1].toString() === a2[y][key2].toString()) {
                                                            allKeysMatch = true;// proven
                                                            console.log('KeyValueMatch=true : ' + key1 + ':' + a1[x][key1] + ' | ' + key2 + ':' + a2[y][key2]);
                                                            break;
                                                        }
                                                    }
                                                }
                                            }
                                            if (ownKeyCount2 === 0) {// if second objects has no keys end early
                                                console.log('-------------- End Check -------------');
                                                return false;
                                            }
                                            // ---------------------------------------------
                                        }
                                    }
                                    console.log('-------------- End Check -------------');
                                }
                            }
                        }
                    }
                    console.log('---------------------------------------');
                    if (noKeys || allKeysMatch) {
                        // If no keys in any objects, assume all objects are {} empty and so the same.
                        // If all keys match without errors, then all object match.
                        return true;
                    } else {
                        return false;
                    }
                }
            }
            console.log('---------------------------------------');
            return true;// one object
        }
        console.log('---------------------------------------');
        return false;// no objects

    } catch (e) {
        if (typeof console !== 'undefined' && console.error) {
            console.error('ERROR : Function ObjectCompare() : ' + e);
        } else if (typeof console !== 'undefined' && console.warn) {
            console.warn('WARNING : Function ObjectCompare() : ' + e);
        } else if (typeof console !== 'undefined' && console.log) {
            console.log('ERROR : Function ObjectCompare() : ' + e);
        }
        return false;
    }
}


// TESTING...

var myObject1 = new Object({test: 1, item: 'hello', name: 'john', f: function(){var x=1;}});
var myObject2 = new Object({item: 'hello', name: 'john', test: 1, f: function(){var x=1;}});
var myObject3 = new Object({name: 'john', test: 1, item: 'hello', f: function(){var x=1;}});

// RETURNS TRUE
//console.log('DO ALL OBJECTS MATCH? ' + ObjectCompare(myObject1, myObject2, myObject3));
Stokely
  • 12,444
  • 2
  • 35
  • 23
1
function getObjectProperties(object, propertiesString = '') {
    let auxPropertiesString = propertiesString;
  
    for (const objectLevel of Object.keys(object).sort((a, b) => a.localeCompare(b))) {
    if (typeof object[objectLevel] === 'object') {
        auxPropertiesString += getObjectProperties(object[objectLevel], auxPropertiesString);
    } else {
        auxPropertiesString += objectLevel;
    }
  }
  
  return auxPropertiesString;
}

function objectsHaveTheSameKeys(objects) {
    const properties = [];
  
  for (const object of objects) {
    properties.push(getObjectProperties(object));
  }
  
  return properties.every(eachProperty => eachProperty === properties[0]);
}

It's a bit rudimentary, but should do the work in case you want to compare properties.

  • When commenting on a possible new solution that already has an accepted answer from 10 years ago, it would help if you provided more context on how this would be better. It would also be helpful if you would explain your answer, how does it work, why does it work like it works, and what does it do what other answers don't. – L00_Cyph3r Jan 30 '23 at 13:58
-1

If you are using underscoreJs then you can simply use _.isEqual function and it compares all keys and values at each and every level of hierarchy like below example.

var object = {"status":"inserted","id":"5799acb792b0525e05ba074c","data":{"workout":[{"set":[{"setNo":1,"exercises":[{"name":"hjkh","type":"Reps","category":"Cardio","set":{"reps":5}}],"isLastSet":false,"index":0,"isStart":true,"startDuration":1469689001989,"isEnd":true,"endDuration":1469689003323,"speed":"00:00:01"}],"setType":"Set","isSuper":false,"index":0}],"time":"2016-07-28T06:56:52.800Z"}};

var object1 = {"status":"inserted","id":"5799acb792b0525e05ba074c","data":{"workout":[{"set":[{"setNo":1,"exercises":[{"name":"hjkh","type":"Reps","category":"Cardio","set":{"reps":5}}],"isLastSet":false,"index":0,"isStart":true,"startDuration":1469689001989,"isEnd":true,"endDuration":1469689003323,"speed":"00:00:01"}],"setType":"Set","isSuper":false,"index":0}],"time":"2016-07-28T06:56:52.800Z"}};

console.log(_.isEqual(object, object1));//return true

If all the keys and values for those keys are same in both the objects then it will return true, otherwise return false.

Mahima Agrawal
  • 1,315
  • 13
  • 13
  • 1
    The problem presented here is to check **only** the keys of the two objects. The values are irrelevant. Your solution checks keys **and** values. – Louis Jul 28 '16 at 10:22
-2

Here is my attempt at validating JSON properties. I used @casey-foster 's approach, but added recursion for deeper validation. The third parameter in function is optional and only used for testing.

//compare json2 to json1
function isValidJson(json1, json2, showInConsole) {

    if (!showInConsole)
        showInConsole = false;

    var aKeys = Object.keys(json1).sort();
    var bKeys = Object.keys(json2).sort();

    for (var i = 0; i < aKeys.length; i++) {

        if (showInConsole)
            console.log("---------" + JSON.stringify(aKeys[i]) + "  " + JSON.stringify(bKeys[i]))

        if (JSON.stringify(aKeys[i]) === JSON.stringify(bKeys[i])) {

            if (typeof json1[aKeys[i]] === 'object'){ // contains another obj

                if (showInConsole)
                    console.log("Entering " + JSON.stringify(aKeys[i]))

                if (!isValidJson(json1[aKeys[i]], json2[bKeys[i]], showInConsole)) 
                    return false; // if recursive validation fails

                if (showInConsole)
                    console.log("Leaving " + JSON.stringify(aKeys[i]))

            }

        } else {

            console.warn("validation failed at " + aKeys[i]);
            return false; // if attribute names dont mactch

        }

    }

    return true;

}
speculees
  • 94
  • 5
  • The OP's question is about comparing keys *and only keys*. Your code will report inequality for some cases if the *values* are different. For instance `isValidJson({a: {a: 1}}, {a: 1}, true)` will complain because `a` is a primitive value in the 2nd object. Also, your algorithm is not commutative. (Flip the two objects in my earlier code and your code reports `true` instead of `false`!) – Louis Jul 28 '16 at 10:35