2

I'm currently having a problem with a deep search in a json object and even though i thought this issue must have been covered alot, I wasn't able to find anything that was really helpful so far (and I actually found alot, also this thread. Maybe I've been looking at code for too long today but it didn't really help me)

Basically what i want is pretty simple. I have a JSON-Object thats pretty deep filled with objects. All i want is a function that returns an array with all objects that contain a given Key-Value-Pair. I made this function to return the first found object which works just fine

 deepSearch: function(Obj, Key, Value){
                    var returned = [];
                    var result = false;
                    var searchObj = function(_Obj, _Key, _Value){
                        if(_Obj[_Key]===_Value){
                            return _Obj;    
                        } else {
                            return false;
                        }
                    }
                    result = searchObj(Obj, Key, Value);

                    $.each(Obj, function(key, value){
                        if(typeof(Obj[key]) === 'object' && Obj[key]!== null && !result)
                            result = customGeneralFunctions.objects.deepSearch(Obj[key], Key, Value);
                            if(result) return result;
                    });
                    return result;
                }

Now I want to change it to return an array contianing all Objects with that pair. I've been trying for a while now and I think it wouldnt be a change too hard but I just can't wrap my head around it. Maybesomeone has an idea that helps me. Thanks in advance and

Greetings Chris

Community
  • 1
  • 1
relief.melone
  • 3,042
  • 1
  • 28
  • 57

4 Answers4

5

A safe deep object search?

Can't let this pass 3 answers with examples, all flawed. And all illustrate some classic Javascript coding got-ya's

null is an Object

UPDATE an answer has been changed.

As the code is no longer visible I will just leave the warning when iterating an object's properties and you use typeof to check if you have an object be careful to check for null as it is also of type "object"

getObject returns to early and fails to find additional objects nested inside objects that meet the condition. Though easily fixed by removing the return it will still throw a TypeError: Cannot read property 'find' of null if the object being searched contains an array with null in it.

for in the indiscriminate iterator

UPDATE an answer has been removed.

I have added the removed code as an example in the snippet below function deepSearch is fatally flawed and will more likely throw a RangeError: Maximum call stack size exceeded error then find the object you are looking for. eg deepSearch({ a:"a"},"id",3);. When using for in you should type check as it will iterate a string as well as an object's properties.

function deepSearch(object, key, value) {
  var filtered = [];
  for (var p in object)
    if (p === key && object[p] === value) filtered.push(object);
    else if (object[p]) filtered = filtered.concat(deepSearch(object[p], key, value));      
  return filtered;
}

Dont trust the callback.

Alex K search passed most tests (within reasonable scope of the question) but only if the code in the form of the comment // tip: here is a good idea to check for hasOwnProperty would have been included.

But that said the function has a flaw (and inefficiency) as it will call predicate on all properties of an object, and I can think of plenty of scenarios in which the function can return many references to the same object eg the reciprocal search for objects with property key NOT with value predicate = (key,val)=>{return key === "id" && val !== 3}.

The search should only add one entry per object thus we should test the object not the properties. We can never trust the callback to do what we expect.

And as it is the accepted answer I should point out that Array.concat should really not be used as it is in this situation. Using closure is much more efficient and allows you to not have to pass the current state to each recursion.


Circular reference.

The flaw to floor them all.

I am not to sure if it is relevant as the question does state that the data is from the form JSON and hence would be free of any circular reference (JSON can not reference).

But I will address the problem and several solutions.

A circular reference is simply an object referencing itself. For example.

var me = {};
me.me = me;

That will crash all the other answers if passed as an argument. Circular references are very common.

Some solutions.

  • First solution is to only accept data in the form of a JSON string and equally return the data as a JSON string (so balance is maintained and the universe does not explode). Thus eliminating any chance of a circular reference.

  • Track recursion depth and set a limit. Though this will stop a callstack overflow it will not prevent the result being flawed as a shallow circular reference can create duplicate object references.

  • The quick down and dirty solution is a simple try catch around a JSON.stringify and throw TypeError("Object can not be searched"); for those on that side of the data bus..

  • The best solution is to decycle the object. Which in this case is very amenable to the actual algorithm we are using. For each unique object that is encountered we place it in an array. If we encounter an object that is in that array we ignore it and move on.


A possible solution.

Thus the general purpose solution, that is safe (I hope) and flexible. Though it is written for ES6 so legacy support will have to be provided in the form of babel or the like. Though it does come with a BUT!

// Log function 
function log(data){console.log(data)}

// The test data
var a = {
    a : "a",
    one : {
        two : {
            find : "me",
            data : "and my data in one.two"
        },
        twoA : {
            four : 4,
            find : "me",
            data : "and my data in one.twoA"
        }
    },
    two : {
        one : {
            one : 1,
            find : "not me",
        },
        two : {
            one : 1,
            two : 1,
            find : "me",
            data : "and my data in two.two"
        },
    },
    anArray : [
        null,0,undefined,/./,new Date(),function(){return hi},
        {
            item : "one",
            find : "Not me",
        },{
            item : "two",
            find : "Not me",
            extra : {
                find : "me",
                data : "I am a property of anArray item 1",
                more : {
                    find : "me",
                    data : "hiding inside me"
                },
            }
        },{
            item : "three",
            find : "me",
            data : "and I am in an array"
        },{
            item : "four",
            find : "me",
            data : "and I am in an array"
        },
    ],
    three : { 
        one : {
            one : 1,
        },
        two : {
            one : 1,
            two : 1,
        },
        three : {
            one : 1,
            two : {
                one : {
                    find : "me",
                    data : "and my data in three.three.two.one"
                }
            }
        }
    },
}

// Add cyclic referance
a.extra = {
    find : "me",
    data : "I am cyclic in nature.",
}
a.extra.cycle = a.extra;
a.extraOne = {
    test : [a],
    self : a,
    findme : a.extra,
};




if(! Object.allWith){
    /*  Non writeable enumerable configurable property of Object.prototype 
        as a function in the form
        Object.allWith(predicate)
        Arguments
            predicate Function used to test the child property takes the argument
               obj the current object to test
            and will return true if the condition is meet 
        Return
            An array of all objects that satisfy the predicate
            
        Example
        
        var test = {a : { key : 10, data: 100}, b : { key : 11, data: 100} };
        var res = test.allWith((obj)=>obj.key === 10);
        // res contains test.a
    */
    Object.defineProperty(Object.prototype, 'allWith', {
        writable : false,
        enumerable : false,
        configurable : false,
        value : function (predicate) {
            var uObjects = [];
            var objects = [];
            if (typeof predicate !== "function") {throw new TypeError("predicate is not a function")}
            (function find (obj) {
                var key;
                if (predicate(obj) === true) {objects.push(obj)}
                for (key of Object.keys(obj)) {
                    let o = obj[key];
                    if (o && typeof o === "object") {
                        if (! uObjects.find(obj => obj === o)) {
                            uObjects.push(o);
                            find(o);
                        }
                    }
                }
            } (this));
            return objects;
        }
    });
}else{
    console.warn("Warn!! Object.allWith already defined.");
}

var res = a.allWith(obj => obj.find === "me");
res.forEach((a,i)=>(log("Item : " + i + " ------------"),log(a)))

Why are you searching through unknown data structures?

It works for all the test cases I could come up with, but that is not at all the definitive test. I added it to the Object.prototype because you should not do that!!! nor use such a function or derivative thereof.

This is the first time I have written such a function, and the reason is that I have never had to write something like that before, I know what the data looks like and I dont have to create dangerous recursive iterators to find what is needed.. If you are writing code and you are not sure of the data you are using there is something wrong in the design of the whole project.

Community
  • 1
  • 1
Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • This, I think, should be an accepted answer. @Chris, what do you think? – Vladislav Rastrusny Feb 16 '17 at 08:20
  • Thanks for that very detailed answer. You really helped alot. I would be happy if those data structures were known. But esenitally what my program does is retrieving a data structure from Catia (a CAD-Software) and buidling up the structure of a constructed component however the user decided to design it, so thats something i have no influence on – relief.melone Feb 20 '17 at 08:05
1

Hopefully this will help you to solve your task. Lets use recursion to search deep into object. Also lets make it more generic.

// search function takes object as a first param and
// a predicate Function as second predicate(key, value) => boolean
function search(obj, predicate) {
    let result = []; 
    for(let p in obj) { // iterate on every property
        // tip: here is a good idea to check for hasOwnProperty
        if (typeof(obj[p]) == 'object') { // if its object - lets search inside it
            result = result.concat(search(obj[p], predicate));
        } else if (predicate(p, obj[p])) 
            result.push(
               obj
            ); // check condition
    }
    return result;
}

Lets test it!

var obj = {
    id: 1,
    title: 'hello world',
    child: {
        id: 2,
        title: 'foobar',
        child: {
            id: 3,
            title: 'i should be in results array '
        }
    },
    anotherInnerObj: {
        id: 3,
        title: 'i should be in results array too!'
    }
};

var result = search(obj, function(key, value) { // im looking for this key value pair
    return key === 'id' && value === 3;
});

Output:

result.forEach(r => console.log(r))
// Object {id: 3, title: "i should be in results array "}
// Object {id: 3, title: "i should be in results array too!"}
Alex K
  • 395
  • 3
  • 10
  • That's an awesome solution. Thanks! – relief.melone Feb 15 '17 at 17:26
  • Your function will return the incorrect result if the predicate is something like `(key,val)=> !(key==="id" && val==3)` as it will return multiple references for the same object. One for each property that passes the test – Blindman67 Feb 15 '17 at 22:15
0

You've created a returned array. First, push the result of searchObj() into it. Then in your loop, if you get a result, concat() it to returned. Finally, return returned at the end of the function. That should do it...

Chris Camaratta
  • 2,729
  • 22
  • 35
  • thanks for the quick reply. I did that before with push. Problem is, i get an array thats as deep as the original object but just filled with empty arrays Now with the concat I just get a single empty array :/ – relief.melone Feb 15 '17 at 15:05
0

You could use a simplified version and

  • check if object not truthy or object is not an object, then return
  • check if given key and value match, then add the actual object to the result set,
  • get the keys and iterate over the properties and call the function again.

At last, the array with the collected objects is returned.

function getObjects(object, key, value) {
    function iter(o) {
        if (!o || typeof o !== 'object') {
            return;
        }
        if (o[key] === value){
            result.push(o);
        }
        Object.keys(o).forEach(function (k) {
            iter(o[k]);
        });
    }

    var result = [];
    iter(object);
    return result;
}

var object = { id: 1, title: 'hello world', child: { id: null, title: 'foobar', child: { id: null, title: 'i should be in results array ' } }, foo: { id: null, title: 'i should be in results array too!' }, deep: [{ id: null, value: 'yo' }, { id: null, value: 'yo2' }] };

console.log(getObjects(object, 'id', null));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • You return too early and will miss objects that are properties of a selected object. eg `{id:3,a : {id:3}}` you will return only one object while it should return two. Also watch out for null. – Blindman67 Feb 15 '17 at 22:18
  • The question states "array with all objects that contain a given Key-Value-Pair." – Blindman67 Feb 15 '17 at 22:50
  • @Blindman67, please see edit. i am not sure about circular references, this was not part of the question. usually objects organised as trees have no circular references. – Nina Scholz Feb 16 '17 at 07:54
  • Include a single DOM object and you instantly have a circular referance. Circular referencing is good practice (eg a double linked list) and it is always good to avoid stack overflows when chasing a tail. I'll ask why put the object test at the start of the function rather than in the forEach loop which would avoid creating a new function context for each non object. – Blindman67 Feb 16 '17 at 08:30
  • because if you use `getObjects(42, 'id', null)` then it throws an exception. – Nina Scholz Feb 16 '17 at 08:33