How can I check that two objects have the same set of property names?
Asked Answered
D

9

83

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;
  }
}
Dentist answered 16/1, 2013 at 21:56 Comment(0)
H
133

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.

Homovec answered 16/1, 2013 at 21:58 Comment(10)
I do not want to check the actual values of the properties, just the property names. sorry for the confusionDentist
that is exactly what i was looking for...new to JS and wasn't sure how to do the property reflection. Thanks!Dentist
+ 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})Cyclone
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.Raphaelraphaela
+AlexG It works now so I guess that's enough more most people -_-Fichu
chai has the keys() method for this: chaijs.com/api/bdd/#method_keysLeucopenia
if you need deep check / nested objects #41802759Courson
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.Guidepost
JSON.stringify also fails for objects with undefined values. Eg: { a: undefined } and { b: undefined } both return "{}" with JSON.stringify()Temikatemp
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.Stans
C
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
Cutlip answered 27/1, 2016 at 21:0 Comment(4)
Awesome comparison!Lucianolucias
Why did I get downvotes? Please write a comment so that I can improve my answer :)Cutlip
In order to return the number of different keys: return objects.reduce((res, object) => res += union.size - Object.keys(object).length, 0);Except
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.42msJudaize
R
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.

Retouch answered 16/1, 2013 at 23:20 Comment(6)
I think this will work too. Very similar to Casey's. ThanksDentist
Doesn't this only check of obj2 has obj1's properties, and not vice versa?Ameliorate
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.Retouch
It looks like it is checking only the first level properties, right?Wootten
@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.Retouch
.every skips the last element, instead of .every use .forEachPerlaperle
D
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));
Des answered 11/5, 2016 at 5:54 Comment(0)
F
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
Flavescent answered 30/7, 2021 at 9:44 Comment(2)
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 falseBight
@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.Flavescent
T
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));
Tamandua answered 18/12, 2022 at 21:37 Comment(0)
L
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.

Leggat answered 25/1, 2023 at 8:41 Comment(1)
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.Phosgenite
S
-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.

Storeroom answered 28/7, 2016 at 8:5 Comment(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.Scholz
C
-3

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;

}
Coplanar answered 3/3, 2016 at 15:27 Comment(1)
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!)Scholz

© 2022 - 2024 — McMap. All rights reserved.