11

I have a javascript object width depth.

I need to know the exact path from this key within the object ex: "obj1.obj2.data1"

I already know the key is data1, the value is 123.

My javascript object look like this

{
    obj1: {
        obj2: {
            data1: 213,
            data2: "1231",
            obj3: {
                data: "milf"
            }
        }
    },
    obj4: {
        description: "toto"
    }
}

How could I achieve that ?

here is a jsfiddle : http://jsfiddle.net/3hvav8xf/8/ I am trying to implement getPath.

Dimitri Kopriwa
  • 13,139
  • 27
  • 98
  • 204
  • 1
    What's the point of doing this? – Ram Aug 20 '14 at 11:47
  • It is for implementing a changelog, the way it is done today makes it easier if we can do it like that – Dimitri Kopriwa Aug 20 '14 at 11:51
  • Is it for mongodb update? –  Aug 20 '14 at 12:00
  • @TamilVendhan no it isn't, it's for a making a json changelog from a version to another – Dimitri Kopriwa Aug 20 '14 at 12:01
  • Do. Not. Reinvent. The. Wheel. There are always people at least as smart as you who have already had the same problem https://github.com/benjamine/jsondiffpatch – Prinzhorn Aug 20 '14 at 12:12
  • 1
    @Prinzhorn I am working on an existing system, which require not to reimplement all the logic. I can't implement a new library, which will require month of tests. Please don't provide me further alternative solutions, the fiddle is just here to demonstrate what I am trying to achieve – Dimitri Kopriwa Aug 20 '14 at 12:19

12 Answers12

12

I think recursive function can help to you (Updated version, to check value)

function path(c, name, v, currentPath, t){
    var currentPath = currentPath || "root";

    for(var i in c){
      if(i == name && c[i] == v){
        t = currentPath;
      }
      else if(typeof c[i] == "object"){
        return path(c[i], name, v, currentPath + "." + i);
      }
    }

    return t + "." + name;
};

console.log(path({1: 2, s: 5, 2: {3: {2: {s: 1, p: 2}}}}, "s", 1));
Thomas Sablik
  • 16,127
  • 7
  • 34
  • 62
Farkhat Mikhalko
  • 3,565
  • 3
  • 23
  • 37
  • 1
    You aren't checking the value, we need to verify that the value is the one we are looking for, for a particular reason, we can have many keys with the same name, but they can't have the same value – Dimitri Kopriwa Aug 20 '14 at 12:29
  • 1
    It seems to work fine in my debugger, while failing with the fiddle: http://jsfiddle.net/3hvav8xf/9/ , I must accept the answer because you totally answer this question – Dimitri Kopriwa Aug 20 '14 at 13:08
  • 1
    Why is `t` a parameter of that function? – Cold_Class Jun 15 '17 at 09:39
  • 1
    @Cold_Class it's because t is set to be the current path of that run of c –  Sep 26 '18 at 02:28
  • 1
    this doesn't work, unless the key-value pair is the first in the object. console.log(path({1: 2, s: 5, 2: {3: {2: {s: 1, p: 2}}}}, "p", 1)); will give you undefined.p – ayang726 Feb 22 '21 at 22:09
5

The following finds the path in any level of nested objects. Also with arrays. It returns all the paths found, which is something you want if you have keys with the same name.

I like this approach because it works with lodash methods get and set out-of-the-box.

function findPathsToKey(options) {
  let results = [];

  (function findKey({
    key,
    obj,
    pathToKey,
  }) {
    const oldPath = `${pathToKey ? pathToKey + "." : ""}`;
    if (obj.hasOwnProperty(key)) {
      results.push(`${oldPath}${key}`);
      return;
    }

    if (obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
      for (const k in obj) {
        if (obj.hasOwnProperty(k)) {
          if (Array.isArray(obj[k])) {
            for (let j = 0; j < obj[k].length; j++) {
              findKey({
                obj: obj[k][j],
                key,
                pathToKey: `${oldPath}${k}[${j}]`,
              });
            }
          }

          if (obj[k] !== null && typeof obj[k] === "object") {
            findKey({
              obj: obj[k],
              key,
              pathToKey: `${oldPath}${k}`,
            });
          }
        }
      }
    }
  })(options);

  return results;
}

findPathsToKey({ obj: objWithDuplicates, key: "d" })
// ["parentKey.arr[0].c.d", "parentKey.arr[1].c.d", "parentKey.arr[2].c.d"]

Try it here - https://jsfiddle.net/spuhb8v7/1/

If you want the result to be a single key (first encountered), you can change the results to be a string and if defined, then return the function with it.

Roland Jegorov
  • 789
  • 1
  • 11
  • 27
4

I ended up with the following function, that works with nested objects/arrays :

function findPath (obj, name, val, currentPath) {
  currentPath = currentPath || ''

  let matchingPath

  if (!obj || typeof obj !== 'object') return

  if (obj[name] === val) return `${currentPath}['${name}']`

  for (const key of Object.keys(obj)) {
    if (key === name && obj[key] === val) {
      matchingPath = currentPath
    } else {
      matchingPath = findPath(obj[key], name, val, `${currentPath}['${key}']`)
    }

    if (matchingPath) break
  }

  return matchingPath
}

const treeData = [{
  id: 1,
  children: [{
    id: 2
  }]
}, {
  id: 3,
  children: [{
    id: 4,
    children: [{
      id: 5
    }]
  }]
}]

console.log(findPath (treeData, 'id', 5))
3

Here you go!

function getPath(obj, value, path) {

    if(typeof obj !== 'object') {
        return;
    }

    for(var key in obj) {
        if(obj.hasOwnProperty(key)) {
            console.log(key);
            var t = path;
            var v = obj[key];
            if(!path) {
                path = key;
            }
            else {
                path = path + '.' + key;
            }
            if(v === value) {
                return path;
            }
            else if(typeof v !== 'object'){
                path = t;
            }
            var res = getPath(v, value, path);
            if(res) {
                return res;
            } 
        }
    }

}

getPath(yourObject, valueYouWantToFindPath);

Rerutns path if found, else returns undefined. I have only tested it with objects & comparison is very strict(ie: used ===).

Update: Updated version that takes key as an argument.

function getPath(obj, key, value, path) {

    if(typeof obj !== 'object') {
        return;
    }

    for(var k in obj) {
        if(obj.hasOwnProperty(k)) {
            console.log(k);
            var t = path;
            var v = obj[k];
            if(!path) {
                path = k;
            }
            else {
                path = path + '.' + k;
            }
            if(v === value) {
                if(key === k) {
                    return path;
                }
                else {
                    path = t;
                }
            }
            else if(typeof v !== 'object'){
                path = t;
            }
            var res = getPath(v, key, value, path);
            if(res) {
                return res;
            } 
        }
    }

}

getPath(yourObject, key, valueYouWantToFindPath);
  • 1
    Thanks, I have tried your fn in my fiddle, you are missing an argument within this function, we know the original object, we know the key we are looking for, we also know her value. Where is the key we are looking for ? I am trying to edit this one to get what I need – Dimitri Kopriwa Aug 20 '14 at 12:28
  • 1
    The above code is not considering the key. It just takes obj & the value you want to find. Let me see if i can modify the code to make use of the key. –  Aug 20 '14 at 12:30
  • Thanks a lot, I must accept this also because it work and you answered right. still not in the fiddle : http://jsfiddle.net/3hvav8xf/10 , @farhatmihalko answer is a bit shorter btw :] – Dimitri Kopriwa Aug 20 '14 at 13:11
  • 1
    didn't work for me either, because of `path = k;` this overwrites path and therefore you'll get a wrong path in the second iteration of the for-loop because path should be still empty. - I changed it up and this one works for me: https://jsfiddle.net/pa5gcvvx/ – Cold_Class Jun 15 '17 at 11:47
  • Answer of @Cold_Class is working for me well. Others - no =( – Kirill Husiatyn Feb 27 '19 at 16:39
1

JSON Object can be handled in JavaScript as associative array.

So You can cycle through and store indexes of "parents" in some variables.

Assume the whole object to be stored in variable called obj.

for( var p1 in obj )

{

   for( var p2 in obj[ p1 ] )
   {
       for( var p3 in obj[ p1 ][ p2 ] )
       {
           // obj[ p1 ][ p2 ][ p3 ] is current node
           // so for Your example it is obj.obj1.obj2.data1
       }
   }

}

Hope answer was helpful.

Community
  • 1
  • 1
Martin
  • 41
  • 1
  • 8
1

I would do this job as follows;

Object.prototype.paths = function(root = [], result = {}) {
  var ok = Object.keys(this);
  return ok.reduce((res,key) => { var path = root.concat(key);
                                  typeof this[key] === "object" &&
                                         this[key] !== null ? this[key].paths(path,res)
                                                            : res[this[key]] == 0 || res[this[key]] ? res[this[key]].push(path)
                                                                                                    : res[this[key]] = [path];
                                  return res;
                                },result);
};

var myObj = {
    obj1: {
        obj2: {
            data1: 213,
            data2: "1231",
            obj3: {
                data: "milf"
            }
        }
    },
    obj4: {
        description: "toto",
        cougars: "Jodi",
        category: "milf"
    }
},
value = "milf",
milfPath = myObj.paths()[value]; // the value can be set dynamically and if exists it's path will be listed.
console.log(milfPath);

A few words of warning: We should be cautious when playing with the Object prototype. Our modification should have the descriptor enumerable = false or it will list in the for in loops and for instance jQuery will not work. (this is how silly jQuery is, since apparently they are not making a hasOwnProperty check in their for in loops) Some good reads are here and here So we have to add this Object method with Object.defineProperty() to make it enumerable = false;. But for the sake of simplicity and to stay in the scope of the question i haven't included that part in the code.

Community
  • 1
  • 1
Redu
  • 25,060
  • 6
  • 56
  • 76
0

Here is a pretty short, and relatively easy to understand function I wrote for retrieving the JSON Path for every property/field on an Object (no matter how deeply nested, or not).

The getPaths(object) function just takes the Object you'd like the JSON Paths for and returns an array of paths. OR, if you would like the initial object to be denoted with a symbol that is different from the standard JSON Path symbol, $, you can call getPaths(object, path), and each JSON Path will begin with the specified path.

For Example: getPaths({prop: "string"}, 'obj'); would return the following JSON Path: obj.prop, rather than $.prop.

See below for a more detailed, in depth example of what getPaths returns, and how it is used.

object = {
  "firstName": "John",
  "lastName": "doe",
  "age": 26,
  "fakeData": true,
  "address": {
    "streetAddress": "fake street",
    "city": "fake city",
    "postalCode": "12345"
  },
  "phoneNumbers": [{
    "type": "iPhone",
    "number": "0123-4567-8888"
  }, {
    "type": "home",
    "number": "0123-4567-8910"
  }]
};

function getPaths(object, path = "$") {
  return Object.entries(object).flatMap(function(o, i) {
    if (typeof o[1] === "object" && !o[1].length) {
      return `${getPaths(o[1], path + '.' + o[0])}`.split(',');
    } else if (typeof o[1] === "object" && o[1].length) {
      return Object.entries(o[1]).flatMap((no, i) => getPaths(no[1], `${path}.${o[0]}[${i}]`));
    } else {
      return `${path}.${o[0]}`;
    }
  });
}
console.log(`%o`, getPaths(object));
0

I really liked Roland Jegorov's answer, but I had a very complex object that I needed to search through and that answer could not account for it.

If you were in a situation like mine you may want to first make sure you have no circular references (or else you'll run into an infinite search). There are a few ways to do this, but I was having to stringify my object to copy it into other windows, so I ended up using this circular replacer: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value

(Update here - I made a small change to the getCircularReplacer function from MDN so it no longer leaves out function references since that is what I was looking for!)

(Update 3 - I also wanted to check on methods of any instances of classes, but I was returning just 'function' too early, so I have adjusted it to include instance methods. I think it finally works as I intended!)

const getCircularReplacer = () => {
  const seen = new WeakSet();
  return (key, value) => {
    if (typeof value === "function") {
      if (value?.prototype) {
        if (seen.has(value.prototype)) {
          return;
        }
        seen.add(value.prototype)
        return value.prototype
      }
      return "function";
    }
    if (typeof value === "object" && value !== null) {
      if (seen.has(value)) {
        return;
      }
      seen.add(value);
    }
    return value;
  };
};

const nonCyclicObject = JSON.parse(JSON.stringify(myComplexObject, getCircularReplacer()));

Then I used this modified version of Roland's answer:

(Update 2: I had to make sure not to return after the key was found as it would always simply return after only calling the function once if the first level of the object had that key)

function findPathsToKey(options) {
  let count = 0;
  let results = [];

  (function findKey({
    key,
    obj,
    pathToKey,
  }) {
    count += 1;
    if (obj === null) return;
    
    const oldPath = `${pathToKey ? pathToKey + "." : ""}`;
    if (Object.hasOwnProperty.call(obj, key)) {
      results.push(`${oldPath}${key}`);
    }
    
    if (typeof obj === "object" && !Array.isArray(obj)) {
      for (const k in obj) {
        if (Object.hasOwnProperty.call(obj, k)) {
          if (Array.isArray(obj[k])) {
            for (let j = 0; j < obj[k].length; j++) {
              findKey({
                obj: obj[k][j],
                key,
                pathToKey: `${oldPath}${k}[${j}]`,
              });
            }
          }

          if (typeof obj[k] === "object") {
            findKey({
              obj: obj[k],
              key,
              pathToKey: `${oldPath}${k}`,
            });
          }
        }
      }
    }
  })(options);

  return { count, results };
};

The count was just to troubleshoot a little bit and make sure it was actually running through the amount of keys I thought it was. Hope this helps any others looking for a solution!

0

⚠️ This code doesn't answer the question but does related: transforms nested object to query object with dot.divided.path as keys and non-object values; compatible with URlSearchParams & qs. Maybe will be useful for someone.

const isPlainObject = (v) => {
  if (Object.prototype.toString.call(v) !== '[object Object]') return false;
  const prototype = Object.getPrototypeOf(v);
  return prototype === null || prototype === Object.prototype;
};

const objectToQueryObject = (obj, path) => {
  return Object.entries(obj).reduce((acc, [key, value]) => {
    const newPath = path ? `${path}.${key}` : key;
    if (isPlainObject(value)) {
      return {
        ...acc,
        ...objectToQueryObject(value, newPath)
      };
    }
    acc[newPath] = value;
    return acc;
  }, {})
};

const queryObjectRaw = {
  value: {
    field: {
      array: {
        '[*]': {
          field2: {
            eq: 'foo',
            ne: 'bar',
          }
        }
      },
      someOtherProp: { in: [1, 2, 3],
        ne: 'baz',
      }
    },
    someOtherField: {
      gt: 123
    },
  },
  otherValue: {
    eq: 2
  },
};

const result = objectToQueryObject(queryObjectRaw);

console.log('result', result);

const queryString = new URLSearchParams(result).toString();

console.log('queryString', queryString);
Georgiy Bukharov
  • 356
  • 3
  • 10
0

If you know only the value and not the key, and want to find all paths with this value use this.
It will find all property with that value, and print the complete path for every founded value.

const createArrayOfKeys = (obj, value) => {
    const result = []
    function iter(o) {
      Object.keys(o).forEach(function(k) {
        if (o[k] !== null && typeof o[k] === 'object') {
          iter(o[k])
          return
        }

        if (o[k]=== value) {
          
          
          result.push(k)
          return
          }
      })
    }

    iter(obj)
    return result
  }

function findPath (obj, name, val, currentPath) {
  currentPath = currentPath || ''

  let matchingPath

  if (!obj || typeof obj !== 'object') return

  if (obj[name] === val) return `${currentPath}/${name}/${val}`

  for (const key of Object.keys(obj)) {
    if (key === name && obj[key] === val) {
      matchingPath = currentPath
    } else {
      matchingPath = findPath(obj[key], name, val, `${currentPath}/${key}`)
    }

    if (matchingPath) break
  }

  return matchingPath
}

const searchMultiplePaths = (obj, value) => {
    const keys = createArrayOfKeys(obj, value)
    console.log(keys);
    keys.forEach(key => {
        console.log(findPath(obj, key, value))
    })
}

var data = { ffs: false, customer: { customer_id: 1544248, z_cx_id: '123456' }, selected_items: { '3600196': [{ id: 4122652, name: 'Essential Large (up to 8\'x10\')', selected: true }] }, service_partner: { id: 3486, name: 'Some String', street: '1234 King St.',  hop: '123456' }, subject: 'Project-2810191 - Orange Juice Stain (Rug)', description: 'Product Type: \n\nIssue: (copy/paste service request details here)\n\nAction Required:', yes: '123456' };

searchMultiplePaths(data, '123456')
dandan
  • 123
  • 1
  • 6
0

I know the post is old but the answers don't really satisfy me.

A simple solution is to add the object path to each object in the structure. Then you can easily read the path when you need it.

let myObject = {
    name: 'abc',
    arrayWithObject: [
        {
            name: "def"
        },
        {
            name: "ghi",
            obj: {
                name: "jkl"
            }
        }
    ],
    array: [15, 'mno'],
    arrayArrayObject: [
        [
            {
                name: '...'
            }
        ]
    ]
}

function addPath(obj, path = [], objectPathKey = '_path') {
    if (Array.isArray(obj)) {
        
        obj.map((item, idx) => addPath(item, [...path, idx]))
        
    } else if (typeof obj === "object") {
        
        obj[objectPathKey] = path;
        
        for (const key in obj) {
            obj[key] = addPath(obj[key], [...path, key])
        }
        
    }
    
    return obj
}

myObject = addPath(myObject);

let changeMe = _.cloneDeep(myObject.arrayWithObject[0])
changeMe.newProp = "NEW"
changeMe.newNested = {name: "new", deeper: {name: "asdasda"}}

changeMe = addPath(changeMe, changeMe._path)

_.set(myObject, changeMe._path, changeMe);

When your updates are done sanitize your object and remove your _path property.

Advantages of this solution:

  • You do the work once
  • you keep your code simple
  • no need for own property checks
  • no cognitive overload
Simon
  • 17
  • 3
-2

I can highly suggest you to use lodash for this problem.

In their documentation this should help you out

// using "_.where" callback shorthand
_.find(characters, { 'age': 1 });
// →  { 'name': 'pebbles', 'age': 1, 'blocked': false }
Arko Elsenaar
  • 1,689
  • 3
  • 18
  • 33