672

I have a data structure like this :

var someObject = {
    'part1' : {
        'name': 'Part 1',
        'size': '20',
        'qty' : '50'
    },
    'part2' : {
        'name': 'Part 2',
        'size': '15',
        'qty' : '60'
    },
    'part3' : [
        {
            'name': 'Part 3A',
            'size': '10',
            'qty' : '20'
        }, {
            'name': 'Part 3B',
            'size': '5',
            'qty' : '20'
        }, {
            'name': 'Part 3C',
            'size': '7.5',
            'qty' : '20'
        }
    ]
};

And I would like to access the data using these variable :

var part1name = "part1.name";
var part2quantity = "part2.qty";
var part3name1 = "part3[0].name";

part1name should be filled with someObject.part1.name 's value, which is "Part 1". Same thing with part2quantity which filled with 60.

Is there anyway to achieve this with either pure javascript or JQuery?

Lawrence Cherone
  • 46,049
  • 7
  • 62
  • 106
Komaruloh
  • 6,841
  • 3
  • 15
  • 7
  • Not sure what you are asking here? You want to be able to query part1.name and have the text "part1.name" returned? Or you want a means to get the value stored within part1.name? – BonyT Jun 27 '11 at 10:27
  • have you tried doing like `var part1name = someObject.part1name;` ` – Rafay Jun 27 '11 at 10:29
  • 1
    @BonyT : I want to query someObject.part1.name and return the value of it ("Part 1"). However, I want the query (I called it "the key") to be stored in a variable 'part1name'. Thanks for your reply. @3nigma : I have certainly do. But that is not my intention. Thanks for the reply. – Komaruloh Jun 27 '11 at 10:42
  • 1
    in the duplicate answer, i love fyr's answer http://stackoverflow.com/questions/8817394/javascript-get-deep-value-from-object-by-passing-path-to-it-as-string – Steve Black Mar 20 '13 at 02:09
  • 1
    See also [Convert JavaScript string in dot notation into an object reference](http://stackoverflow.com/q/6393943/1048572) – Bergi Dec 15 '15 at 12:45
  • Have a look at JSONPath: https://github.com/jayway/JsonPath – mplungjan Aug 25 '16 at 09:58
  • This works nice https://www.npmjs.com/package/object-path – Gene Bo Aug 29 '19 at 23:47

46 Answers46

675

I just made this based on some similar code I already had, it appears to work:

Object.byString = function(o, s) {
    s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
    s = s.replace(/^\./, '');           // strip a leading dot
    var a = s.split('.');
    for (var i = 0, n = a.length; i < n; ++i) {
        var k = a[i];
        if (k in o) {
            o = o[k];
        } else {
            return;
        }
    }
    return o;
}

Usage::

Object.byString(someObj, 'part3[0].name');

See a working demo at http://jsfiddle.net/alnitak/hEsys/

EDIT some have noticed that this code will throw an error if passed a string where the left-most indexes don't correspond to a correctly nested entry within the object. This is a valid concern, but IMHO best addressed with a try / catch block when calling, rather than having this function silently return undefined for an invalid index.

Alnitak
  • 334,560
  • 70
  • 407
  • 495
  • Your answer is exactly what I need. It work like a charm. However Felix Kling came up with different way and work as good as your answer. I just feel like I have to mention his answer as the correct one either. Anyway, thanks alot for the solution you gave me. – Komaruloh Jun 27 '11 at 11:28
  • What if the value is null? How can I handle it? – Kimchi Man Aug 05 '14 at 14:23
  • 40
    This works beautifully. Please contribute this to the internet by wrapping it as a node package. – t3dodson Jan 13 '15 at 22:38
  • @pero thanks for the fix - I should have tested my change first :( – Alnitak Mar 27 '15 at 22:23
  • @hsz I rolled back your edit because it's not only the `object` types that can have properties. – Alnitak Apr 30 '15 at 18:56
  • @Alnitak Hi. nice solution. however the code in the jsfiddle doesn't work as expected whereas the code in your answer - does. http://i.imgur.com/hTMwxOC.png – Royi Namir Jul 05 '15 at 11:12
  • 18
    @t3dodson I just did: https://github.com/capaj/object-resolve-path just be aware that this doesn't play nice when your property name contains '[]' in itself. Regex will replace it with '.' and it doesn't work as expected – Capaj Jul 30 '15 at 21:57
  • 52
    great stuff; using the lodash library, one can also do: ```_.get(object, nestedPropertyString);``` – ian Aug 13 '15 at 12:49
  • I had this issue: Having a property with a dot inside. So I updated this answer: http://jsfiddle.net/hEsys/359/ – helpse Aug 25 '15 at 14:10
  • 21
    This will probably get lost in the sea of comments, however it errors if you try and address a property that doesn't exist. So `'part3[0].name.iDontExist'`. Adding a check to see if `o` is an object in the `if in` fixes the issue. (How you go about that is up-to you). See updated fiddle: http://jsfiddle.net/hEsys/418/ – ste2425 Nov 17 '15 at 11:12
  • Really great! I have one question... dont i encounter the "object-iteration-problem" (See: http://stackoverflow.com/questions/500504/why-is-using-for-in-with-array-iteration-such-a-bad-idea), when i use this approach to iterate an Array with "var x in array"? – Okyo Dec 07 '15 at 12:31
  • 1
    @Okyo only if you've unsafely added a method to `Array.prototype` as an enumerable property. Sometimes using `for .. in` can actually be better, because it enumerates over both the numeric indices and any additional string properties that were deliberately added to the array. In any event, this code doesn't actually use `for .. in`. – Alnitak Dec 07 '15 at 13:00
  • Ah, that's interesting! I thougt using if (k in o) {} leads to a similar problem. But now i understand! Thanx alot... – Okyo Dec 07 '15 at 14:50
  • Useful! I was wondering how to go from a "leveled String" to an element inside a hash table – yngrdyn Mar 22 '16 at 20:47
  • I played with your code and turned it around to be used for storing a value in to a nested object, which is referred by string. Can be found here: https://jsfiddle.net/jaakkokarhu/omm2v8ty/ – Jaakko Karhu Jun 23 '16 at 12:21
  • Awsome thanks, I added by the if (k in o) { statement a null check for o, if for some reason the proprty is null the code crashes by adding a null check this always works ( if (o != null && k in o ) {) – Ivan Sander de Jong Jul 05 '16 at 10:23
  • 1
    I like to add: `if (!s) { return o; }` at the beginning. – Tarion Nov 04 '16 at 11:03
  • How to set value? – Raz Buchnik Oct 21 '18 at 06:28
  • 1
    The for loop could be replaced with a reduce `a.reduce((so, k) => so[k], o)` – Joe Feb 26 '19 at 16:25
  • @Joe it could, but then a bunch of folks would no longer be able to understand it. Also, this code long predates ES6. – Alnitak Feb 26 '19 at 22:32
  • @Alnitak I find it a lot easier to understand (and if understandability was a concern, why name all your variables a single letter?). I wasn't saying your answer is incorrect, or even suggesting you change it in any way - I found it very useful. Just saying that there is a more susinct version that many people might prefer. ES6 does exists now. – Joe Feb 27 '19 at 08:41
  • Great! How do you guys think injecting this method into Object.prototype? `Object.prototype.$accessProperty = function(s) { // method body }` So we can call this functionality by `myObject.$accessProperty('array1[0].name1')`, which looks more natural – zmou-d Sep 02 '19 at 05:16
  • There is a better way to do this using `lodash`. Please see Ian's or my answer below. – ThatGuyRob Oct 20 '19 at 22:43
  • 3
    @ThatGuyRob introducing a third party library is not always "better", and in any event that method didn't even exist when I wrote this answer. – Alnitak Oct 21 '19 at 16:44
  • @Alnitak, I actually...totally agree with you. I guess I should have been more explicit. More and more applications we work on these days are based on a framework. Many of which already include `lodash`. In many modern frameworks, nothing new is introduced by using it. Quite a lot has changed in recent years. I just wanted to share a recent/modern solution that solved my problem effectively. Happy Coding, @Alnitak. – ThatGuyRob Oct 22 '19 at 17:15
  • Very nice. Suggestion: add to your answer the use of a prototype: Object.prototype.access = function (s) { return Object.byString(this, s) } to use it like someObject.access("'part3[0].name"). – exchange Aug 17 '20 at 15:25
  • Wouldn't it make it slightly more efficient if you returned `o = o[k];` like `return o[k];` to get out of the loop, or at least put a `break;` in there? – Benji A. Feb 19 '21 at 14:36
  • 1
    @BenjiA. no - the iteration has to continue there, not stop. – Alnitak Feb 19 '21 at 22:31
  • it does not work for me `a[0].b[1]` when `b` has only 1 element – Mohammad Arshad Alam Jul 28 '21 at 05:13
  • @Arshad if `b` has only one element you need `b[0]`, not `b[1]`. – Alnitak Jul 28 '21 at 09:52
  • @Alnitak, I am expecting `undefined` in that scenario. But it is return whole `b` element – Mohammad Arshad Alam Jul 29 '21 at 09:02
  • Works like a charm, and even added a try catch to catch if the path passed is not present on the object. – Poujhit Sep 22 '22 at 12:19
296

This is now supported by lodash using _.get(obj, property). See https://lodash.com/docs#get

Example from the docs:

var object = { 'a': [{ 'b': { 'c': 3 } }] };

_.get(object, 'a[0].b.c');
// → 3

_.get(object, ['a', '0', 'b', 'c']);
// → 3

_.get(object, 'a.b.c', 'default');
// → 'default'
Ian Walker-Sperber
  • 3,661
  • 2
  • 14
  • 18
  • 12
    This should be the only accepted answer, because this is the only one working for both dot and bracket syntax and It doesn't fail, when we have '[]' in the string of a key in the path. – Capaj Aug 04 '15 at 02:12
  • 12
    This. Plus, it supports `_.set(...)` – Josh C. Aug 22 '17 at 06:23
  • what happes if the objet is not found? – DDave Oct 31 '17 at 16:58
  • @DDave if the value passed as the object is undefined or not an object, `_.get` will show the same behavior as when no key is found in the provided object. eg `_.get(null, "foo") -> undefined`, `_.get(null, "foo", "bar") -> "bar"`. However this behavior is not defined in the docs so subject to change. – Ian Walker-Sperber Nov 02 '17 at 00:40
  • 22
    @Capaj you kiddin'? And who doesn't want/can't use lodash? – Andre Figueiredo Feb 19 '18 at 21:21
  • 5
    @ Capaj ... I don't want to use an external library for a few lines of code. – run_the_race Mar 29 '22 at 11:02
259

This is the solution I use:

function resolve(path, obj=self, separator='.') {
    var properties = Array.isArray(path) ? path : path.split(separator)
    return properties.reduce((prev, curr) => prev?.[curr], obj)
}

Example usage:

// accessing property path on global scope
resolve("document.body.style.width")
// or
resolve("style.width", document.body)

// accessing array indexes
// (someObject has been defined in the question)
resolve("part3.0.size", someObject) // returns '10'

// accessing non-existent properties
// returns undefined when intermediate properties are not defined:
resolve('properties.that.do.not.exist', {hello:'world'})

// accessing properties with unusual keys by changing the separator
var obj = { object: { 'a.property.name.with.periods': 42 } }
resolve('object->a.property.name.with.periods', obj, '->') // returns 42

// accessing properties with unusual keys by passing a property name array
resolve(['object', 'a.property.name.with.periods'], obj) // returns 42

Limitations:

  • Can't use brackets ([]) for array indices—though specifying array indices between the separator token (e.g., .) works fine as shown above.
speigg
  • 2,740
  • 1
  • 13
  • 12
  • 7
    using reduce is an excellent solution (one can also use `_.reduce()` from the underscore or lodash library) – Alp May 22 '14 at 14:51
  • 5
    I think `self` is probably undefined here. Do you mean `this`? – Platinum Azure Jun 17 '14 at 03:37
  • lodash + typescript implementation: `_.reduce(path.split('.'), (previous, current) => !safe ? previous[current] : (previous ? previous[current] : undefined), target || self);` – Slavo Vojacek Jun 10 '15 at 22:39
  • 3
    Here's my complement to set values by path: http://pastebin.com/jDp5sKT9 – mroach Jan 02 '17 at 08:42
  • It would be better to use a default object in the signature, i.e. function resolve(path, obj={}) and remove the "|| self" part, which refers to the window (global) object. – ingdc Jul 27 '18 at 08:31
  • 1
    @SC1000 good idea. This answer was written before default parameters were available in most browsers. I'll update it to "function resolve(path, obj=self)", since referencing the global object as a default is intentional. – speigg Aug 15 '18 at 00:29
  • I parametrized the "separator" value as well, in order to support non-typical property names. – speigg Aug 15 '18 at 00:41
  • you can also do: `function resolve(properties, obj=self) { return properties.reduce((prev, curr) => prev && prev[curr], obj) }` and you can use like this `resolve(["style","width"], document.body)` – Jaider Feb 04 '19 at 07:31
  • You could add a `vanilla js` note since the question has `jquery` in title. – kursus Mar 05 '20 at 00:39
  • 3
    @AdamPlocher I know this is old now, but I converted this to typescript as follows: `export function resolvePath(path: string | string[], obj: any, separator = '.') { const properties = Array.isArray(path) ? path : path.split(separator); return properties.reduce((prev, curr) => prev && prev[curr], obj); }` – crush Apr 13 '20 at 07:12
  • does not work on the single prop`resolve("prop1", obj)` – Sergino Oct 22 '21 at 11:30
  • `prev && prev[curr]` can be rewritten as `prev?.[curr]` – Christian Vincenzo Traina Mar 31 '22 at 15:12
  • Tasty and easy to understand – Mark Aug 12 '22 at 05:41
234

ES6: Only one line in Vanila JS (it return null if don't find instead of giving error):

'path.string'.split('.').reduce((p,c)=>p&&p[c]||null, MyOBJ)

Or example:

'a.b.c'.split('.').reduce((p,c)=>p&&p[c]||null, {a:{b:{c:1}}})

With Optional chaining operator:

'a.b.c'.split('.').reduce((p,c)=>p?.[c], {a:{b:{c:1}}})

For a ready to use function that also recognizes false, 0 and negative number and accept default values as parameter:

const resolvePath = (object, path, defaultValue) => path
   .split('.')
   .reduce((o, p) => o ? o[p] : defaultValue, object)

Example to use:

resolvePath(window,'document.body') => <body>
resolvePath(window,'document.body.xyz') => undefined
resolvePath(window,'document.body.xyz', null) => null
resolvePath(window,'document.body.xyz', 1) => 1

Bonus:

To set a path (Requested by @rob-gordon) you can use:

const setPath = (object, path, value) => path
   .split('.')
   .reduce((o,p,i) => o[p] = path.split('.').length === ++i ? value : o[p] || {}, object)

Example:

let myVar = {}
setPath(myVar, 'a.b.c', 42) => 42
console.log(myVar) => {a: {b: {c: 42}}}

Access array with []:

const resolvePath = (object, path, defaultValue) => path
   .split(/[\.\[\]\'\"]/)
   .filter(p => p)
   .reduce((o, p) => o ? o[p] : defaultValue, object)

Example:

const myVar = {a:{b:[{c:1}]}}
resolvePath(myVar,'a.b[0].c') => 1
resolvePath(myVar,'a["b"][\'0\'].c') => 1
Adriano Spadoni
  • 4,540
  • 1
  • 25
  • 25
  • 3
    I love this technique. This is really messy but I wanted to use this technique for assignment. `let o = {a:{b:{c:1}}}; let str = 'a.b.c'; str.split('.').splice(0, str.split('.').length - 1).reduce((p,c)=>p&&p[c]||null, o)[str.split('.').slice(-1)] = "some new value";` – rob-gordon Jun 28 '17 at 17:15
  • 5
    I like the idea of using reduce but your logic seems off for `0`, `undefined` and `null` values. `{a:{b:{c:0}}}` returns `null` instead of `0`. Perhaps explicitly checking for null or undefined will clear up these issues. `(p,c)=>p === undefined || p === null ? undefined : p[c]` – SmujMaiku Oct 15 '17 at 17:31
  • 1
    Hi @SmujMaiku, the "ready to use" function return correctly for '0', 'undefined' and 'null', I just tested on the console: resolvePath({a:{b:{c:0}}},'a.b.c',null) => 0; It check if the key exists instead of the value itself which avoid more than one check – Adriano Spadoni Oct 16 '17 at 09:18
  • here defaultValue did not work, using `Reflect.has(o, k) ? ...` (ES6 [Reflect.has](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/has)) worked though – Andre Figueiredo Feb 23 '18 at 12:31
  • `setPath` will set the value on the first occurrence of the last selector. For example `let o = {}; setPath(o, 'a.a', 0)` results in `{a: 0}` rather than `{a: {a: 0}}`. see [this](https://stackoverflow.com/a/46008856/6837399) post for a solution. – pkfm Mar 24 '19 at 01:34
  • I can't get passed "exemple". – ADJenks Sep 11 '19 at 18:34
  • 1
    @AdrianoSpadoni Sorry, I feel I wasn't very polite in the way I pointed it out. Have a great day. – ADJenks Sep 24 '19 at 17:59
  • Whoa, thank you very much for introducing me to the optional chaining operator! It looks awesome, and seems like the browser support is good as well! – Isti115 Sep 08 '20 at 12:31
  • 1
    Note the versions with `defaultValue` will still return `undefined` in some circumstances—e.g. `resolvePath({profile: {name: 'Bob'}}, 'profile.email', 'not set')`. To fix this, the final line should be `.reduce((o, p) => o?.[p] ?? defaultValue, object)` – dsl101 Jul 27 '21 at 14:19
  • default value doesn;t work, should put it after reducer, not inside – AuthorProxy Nov 26 '21 at 12:42
  • `setPath` -> ... `.reduce((o,p,i,{length:l}) => o[p] = l === ++i ? value : o[p] || {}, object)` – Mister Jojo Jun 30 '22 at 12:11
  • Great answer!! For typescript I had to tweak it thusly: `const resolvePath = ( object: Record, path: string, defaultValue: Object = {}, ): Object => path.split(".").reduce((o, p) => (o ? o[p] : defaultValue), object);` – Jannie Theunissen Dec 10 '22 at 13:22
67

You'd have to parse the string yourself:

function getProperty(obj, prop) {
    var parts = prop.split('.');

    if (Array.isArray(parts)) {
        var last = parts.pop(),
        l = parts.length,
        i = 1,
        current = parts[0];

        while((obj = obj[current]) && i < l) {
            current = parts[i];
            i++;
        }

        if(obj) {
            return obj[last];
        }
    } else {
        throw 'parts is not valid array';
    }
}

This required that you also define array indexes with dot notation:

var part3name1 = "part3.0.name";

It makes the parsing easier.

DEMO

d.danailov
  • 9,594
  • 4
  • 51
  • 36
Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
  • @Felix Kling : Your solution does provide me with what I need. And I thank you alot for that. But Alnitak also provide different ways and seem to work either. Since I can only choose one answer, I will choose Alnitak answer. Not that his solution is better than you or something like that. Anyway, I really appreciate your solution and effort you gave. – Komaruloh Jun 27 '11 at 11:25
  • @Felix FWIW - converting from `[]` syntax to property syntax is pretty trivial. – Alnitak Jun 27 '11 at 16:19
  • I like this answer because I can give my users a simpler format for the paths - using dot notation for indexes instead of brackets. Thanks! – hikaru May 01 '14 at 16:02
  • 5
    If you change the while loop to `while (l > 0 && (obj = obj[current]) && i < l)` then this code works for strings without dots as well. – Snea Aug 17 '14 at 06:18
  • 1
    Honestly, this is the better answer because you can actually change the value of obj[last] but you can't change the value if you did it another way. – CuddleBunny Jun 01 '15 at 16:12
  • Might also improve the answer: you can write a prototype Object.prototype.access = function (aString) { return getProperty(this, aString) } and then you can use it like someObject.access("part3.0.name"). Furthermore, you can add the two lines prop = prop.replace(/\[(\w+)\]/g, '.$1'); prop = prop.replace(/^\./, ''); of the first answer to be able to use someObject.access("part3[0].name") – exchange Aug 17 '20 at 15:23
  • I actually found a problem in the above code: It does not work for zero-level objects, for example getProperty(someObject, "part1") returns undefined. I corrected it with the following code: if (parts[0] != undefined) { current = parts[0] } else { current = last } while (i <= l) { obj = obj[current] current = parts[i] i++ } that should be inserted instead of current = parts[0]; while((obj = obj[current]) && i < l) { current = parts[i]; i++; } – exchange Aug 17 '20 at 17:19
  • This is more flexible than others :D – Kavinda Jayakody Dec 16 '20 at 07:00
43

Works for arrays / arrays inside the object also. Defensive against invalid values.

/**
 * Retrieve nested item from object/array
 * @param {Object|Array} obj
 * @param {String} path dot separated
 * @param {*} def default value ( if result undefined )
 * @returns {*}
 */
function path(obj, path, def){
    var i, len;

    for(i = 0,path = path.split('.'), len = path.length; i < len; i++){
        if(!obj || typeof obj !== 'object') return def;
        obj = obj[path[i]];
    }

    if(obj === undefined) return def;
    return obj;
}

//////////////////////////
//         TEST         //
//////////////////////////

var arr = [true, {'sp ace': true}, true]

var obj = {
  'sp ace': true,
  arr: arr,
  nested: {'dotted.str.ing': true},
  arr3: arr
}

shouldThrow(`path(obj, "arr.0")`);
shouldBeDefined(`path(obj, "arr[0]")`);
shouldBeEqualToNumber(`path(obj, "arr.length")`, 3);
shouldBeTrue(`path(obj, "sp ace")`);
shouldBeEqualToString(`path(obj, "none.existed.prop", "fallback")`, "fallback");
shouldBeTrue(`path(obj, "nested['dotted.str.ing'])`);
<script src="https://cdn.rawgit.com/coderek/e7b30bac7634a50ad8fd/raw/174b6634c8f57aa8aac0716c5b7b2a7098e03584/js-test.js"></script>
Endless
  • 34,080
  • 13
  • 108
  • 131
TheZver
  • 1,502
  • 2
  • 14
  • 19
  • 11
    Thanks this is the best and most performant answer - http://jsfiddle.net/Jw8XB/1/ – Dominic Jul 31 '13 at 23:00
  • @Endless, I'd like to emphasize the path should separate the items with dots. Braces won't work. I.e. to access first item in array use "0.sp ace". – TheZver Mar 27 '16 at 11:32
32

This will probably never see the light of day... but here it is anyway.

  1. Replace [] bracket syntax with .
  2. Split on . character
  3. Remove blank strings
  4. Find the path (otherwise undefined)

(For finding a path to an object, use this pathTo solution.)

// "one liner" (ES6)

const deep_value = (obj, path) => 
path
    .replace(/\[|\]\.?/g, '.')
    .split('.')
    .filter(s => s)
    .reduce((acc, val) => acc && acc[val], obj);
    
// ... and that's it.

var someObject = {
    'part1' : {
        'name': 'Part 1',
        'size': '20',
        'qty' : '50'
    },
    'part2' : {
        'name': 'Part 2',
        'size': '15',
        'qty' : '60'
    },
    'part3' : [
        {
            'name': 'Part 3A',
            'size': '10',
            'qty' : '20'
        }
        // ...
    ],
    'pa[rt3' : [
        {
            'name': 'Part 3A',
            'size': '10',
            'qty' : '20'
        }
        // ...
    ]
};

console.log(deep_value(someObject, "part1.name"));               // Part 1
console.log(deep_value(someObject, "part2.qty"));                // 60
console.log(deep_value(someObject, "part3[0].name"));            // Part 3A
console.log(deep_value(someObject, "part3[0].....name"));        // Part 3A - invalid blank paths removed
console.log(deep_value(someObject, "pa[rt3[0].name"));           // undefined - name does not support square brackets
Nick Grealy
  • 24,216
  • 9
  • 104
  • 119
  • Note that this will swallow a lot of invalid paths silently, like `...one...two...`. If one is to do smart processing, you cannot just replace `[]` with `.`, you have to remove every `]`, then replace every `[` that's not at the start with `.`, and then remove all `[` left. – vitaly-t Dec 25 '20 at 11:52
  • @vitaly-t - correct. See `#3 - remove blank strings` - the solution treats blank paths as invalid. And also correct, there is no path validation or support for square brackets in fields names. For a more robust solution, please consider using a library e.g. https://www.npmjs.com/package/jsonpath-plus or https://stedolan.github.io/jq/ – Nick Grealy Dec 25 '20 at 23:40
  • 1
    Yes, there are plenty of libraries out there today that do verbose syntax parsing. Unfortunately, it comes at a cost, being many times slower than the simple index approach of `a.0.b.1`, which is natural for JavaScript, and significantly faster. A simple split suffices. – vitaly-t Dec 26 '20 at 10:36
  • Absolutely right. It's a trade off. If you can control the inputs (e.g. control use of square brackets) all the better - you'll save yourself loading bloated libraries. – Nick Grealy Dec 29 '20 at 03:20
  • I have really tried that, within my own [path-value](https://github.com/vitaly-t/path-value), and while the code for processing square brackets wasn't really bloating, it always had a dramatic performance impact. So in the end I decided not to support it. There is just no way you can parse full syntax quickly in JavaScript. You will end up with code that's 10 times or more slower than without it. – vitaly-t Dec 29 '20 at 13:07
  • Awesome. Now how do I store a value to that path instead of fetching it? :) – HymnZzy Jan 22 '21 at 15:52
  • @HymnZzy simple! Just do a split on the last "." i.e. get the parent object (using the parent path e.g. `var parent = deep_value(someObject, "part3[0]")`, then set the field using child string. `parent["name"] = 'Nick!'`. – Nick Grealy Jan 23 '21 at 15:26
  • It giving me: `Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'unknown'.` – Nicke Manarin Nov 08 '21 at 14:44
  • 1
    @NickeManarin - you're using Typescript, not Javascript. This issue is unrelated, however I believe it's complaining because you haven't assigned types. e.g. `(obj: any, path: string) => etc` – Nick Grealy Nov 08 '21 at 15:02
31

using eval:

var part1name = eval("someObject.part1.name");

wrap to return undefined on error

function path(obj, path) {
    try {
        return eval("obj." + path);
    } catch(e) {
        return undefined;
    }
}

http://jsfiddle.net/shanimal/b3xTw/

Please use common sense and caution when wielding the power of eval. It's a bit like a light saber, if you turn it on there's a 90% chance you'll sever a limb. Its not for everybody.

Shanimal
  • 11,517
  • 7
  • 63
  • 76
  • 8
    Whether or not eval is a good idea depends on where the property string data is coming from. I doubt you have any reason to be concerned for hackers breaking in via a static "var p='a.b.c';eval(p);" type call. It's a perfectly fine idea for that. – James Wilkins Aug 21 '14 at 22:39
26

You can manage to obtain the value of a deep object member with dot notation without any external JavaScript library with the following simple trick:

function objectGet(obj, path) { return new Function('_', 'return _.' + path)(obj); };

In your case to obtain value of part1.name from someObject just do:

objectGet(someObject, 'part1.name');

Here is a simple fiddle demo: https://jsfiddle.net/harishanchu/oq5esowf/

Harish Ambady
  • 12,525
  • 4
  • 29
  • 54
  • 3
    function deep_value ( obj, path ) { return new Function( 'o', 'return o.' + path )( obj ); } – ArcangelZith Jul 29 '16 at 04:59
  • Looked promising, but it seems to be using `eval` under the hood, and is rejected if you have CSP unsafe-eval protected. `Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script` – scipilot Sep 24 '21 at 10:04
15

It's a one liner with lodash.

const deep = { l1: { l2: { l3: "Hello" } } };
const prop = "l1.l2.l3";
const val = _.reduce(prop.split('.'), function(result, value) { return result ? result[value] : undefined; }, deep);
// val === "Hello"

Or even better...

const val = _.get(deep, prop);

Or ES6 version w/ reduce...

const val = prop.split('.').reduce((r, val) => { return r ? r[val] : undefined; }, deep);

Plunkr

James
  • 2,823
  • 22
  • 17
12

I think you are asking for this:

var part1name = someObject.part1.name;
var part2quantity = someObject.part2.qty;
var part3name1 =  someObject.part3[0].name;

You could be asking for this:

var part1name = someObject["part1"]["name"];
var part2quantity = someObject["part2"]["qty"];
var part3name1 =  someObject["part3"][0]["name"];

Both of which will work


Or maybe you are asking for this

var partName = "part1";
var nameStr = "name";

var part1name = someObject[partName][nameStr];

Finally you could be asking for this

var partName = "part1.name";

var partBits = partName.split(".");

var part1name = someObject[partBits[0]][partBits[1]];
Hogan
  • 69,564
  • 10
  • 76
  • 117
  • I think OP's asking for the last solution. However, strings don't have `Split` method, but rather `split`. – duri Jun 27 '11 at 10:37
  • Actualy I was asking the last one. The partName variable is filled with string indicating the key-structure to value. Your solution seems makes sense. However I may need to modify for extended depth in the data, like 4-5 level and more. And I am wondering if I can treat the array and object uniformly with this? – Komaruloh Jun 27 '11 at 10:38
10

Instead of trying to emulate JS syntax which you will have to spend a bunch of compute parsing, or just get wrong/forget things like a bunch of these answers (keys with .s in, anyone?), just use an array of keys.

var part1name     = Object.get(someObject, ['part1', 'name']);
var part2quantity = Object.get(someObject, ['part2', 'qty']);
var part3name1    = Object.get(someObject, ['part3', 0, 'name']);

answer

If you need to use a single string instead, simply JSONify it.
Another improvement in this method is that you can delete/set the root level object.

function resolve(obj, path) {
    let root = obj = [obj];
    path = [0, ...path];
    while (path.length > 1)
        obj = obj[path.shift()];
    return [obj, path[0], root];
}
Object.get = (obj, path) => {
    let [parent, key] = resolve(obj, path);
    return parent[key];
};
Object.del = (obj, path) => {
    let [parent, key, root] = resolve(obj, path);
    delete parent[key];
    return root[0];
};
Object.set = (obj, path, value) => {
    let [parent, key, root] = resolve(obj, path);
    parent[key] = value;
    return root[0];
};

Demo of other features:
demonstration

The bob = for .set(/.del( isn't necessary unless your path might be empty (manipulating the root object).
I prove that I don't clone the object by using steve to keep a reference to the original and checking bob == steve //true after that first .set(

Hashbrown
  • 12,091
  • 8
  • 72
  • 95
9

Just in case, anyone's visiting this question in 2017 or later and looking for an easy-to-remember way, here's an elaborate blog post on Accessing Nested Objects in JavaScript without being bamboozled by

Cannot read property 'foo' of undefined error

Access Nested Objects Using Array Reduce

Let's take this example structure

const user = {
    id: 101,
    email: 'jack@dev.com',
    personalInfo: {
        name: 'Jack',
        address: [{
            line1: 'westwish st',
            line2: 'washmasher',
            city: 'wallas',
            state: 'WX'
        }]
    }
}

To be able to access nested arrays, you can write your own array reduce util.

const getNestedObject = (nestedObj, pathArr) => {
    return pathArr.reduce((obj, key) =>
        (obj && obj[key] !== undefined) ? obj[key] : undefined, nestedObj);
}

// pass in your object structure as array elements
const name = getNestedObject(user, ['personalInfo', 'name']);

// to access nested array, just pass in array index as an element the path array.
const city = getNestedObject(user, ['personalInfo', 'address', 0, 'city']);
// this will return the city from the first address item.

There is also an excellent type handling minimal library typy that does all this for you.

With typy, your code will look like this

const city = t(user, 'personalInfo.address[0].city').safeObject;

Disclaimer: I am the author of this package.

Dinesh Pandiyan
  • 5,814
  • 2
  • 30
  • 49
  • 1
    I think you meant `obj && obj[key] !== undefined` instead of `obj && obj[key] !== 'undefined'` – nzn Jun 14 '23 at 15:02
7

Here I offer more ways, which seem faster in many respects:

Option 1: Split string on . or [ or ] or ' or ", reverse it, skip empty items.

function getValue(path, origin) {
    if (origin === void 0 || origin === null) origin = self ? self : this;
    if (typeof path !== 'string') path = '' + path;
    var parts = path.split(/\[|\]|\.|'|"/g).reverse(), name; // (why reverse? because it's usually faster to pop off the end of an array)
    while (parts.length) { name=parts.pop(); if (name) origin=origin[name]; }
    return origin;
}

Option 2 (fastest of all, except eval): Low level character scan (no regex/split/etc, just a quick char scan). Note: This one does not support quotes for indexes.

function getValue(path, origin) {
    if (origin === void 0 || origin === null) origin = self ? self : this;
    if (typeof path !== 'string') path = '' + path;
    var c = '', pc, i = 0, n = path.length, name = '';
    if (n) while (i<=n) ((c = path[i++]) == '.' || c == '[' || c == ']' || c == void 0) ? (name?(origin = origin[name], name = ''):(pc=='.'||pc=='['||pc==']'&&c==']'?i=n+2:void 0),pc=c) : name += c;
    if (i==n+2) throw "Invalid path: "+path;
    return origin;
} // (around 1,000,000+/- ops/sec)

Option 3: (new: option 2 expanded to support quotes - a bit slower, but still fast)

function getValue(path, origin) {
    if (origin === void 0 || origin === null) origin = self ? self : this;
    if (typeof path !== 'string') path = '' + path;
    var c, pc, i = 0, n = path.length, name = '', q;
    while (i<=n)
        ((c = path[i++]) == '.' || c == '[' || c == ']' || c == "'" || c == '"' || c == void 0) ? (c==q&&path[i]==']'?q='':q?name+=c:name?(origin?origin=origin[name]:i=n+2,name='') : (pc=='['&&(c=='"'||c=="'")?q=c:pc=='.'||pc=='['||pc==']'&&c==']'||pc=='"'||pc=="'"?i=n+2:void 0), pc=c) : name += c;
    if (i==n+2 || name) throw "Invalid path: "+path;
    return origin;
}

JSPerf: http://jsperf.com/ways-to-dereference-a-delimited-property-string/3

"eval(...)" is still king though (performance wise that is). If you have property paths directly under your control, there shouldn't be any issues with using 'eval' (especially if speed is desired). If pulling property paths "over the wire" (on the line!? lol :P), then yes, use something else to be safe. Only an idiot would say to never use "eval" at all, as there ARE good reasons when to use it. Also, "It is used in Doug Crockford's JSON parser." If the input is safe, then no problems at all. Use the right tool for the right job, that's it.

Community
  • 1
  • 1
James Wilkins
  • 6,836
  • 3
  • 48
  • 73
7

AngularJS

Speigg's approach is very neat and clean, though I found this reply while searching for the solution of accessing AngularJS $scope properties by string path and with a little modification it does the job:

$scope.resolve = function( path, obj ) {
    return path.split('.').reduce( function( prev, curr ) {
        return prev[curr];
    }, obj || this );
}

Just place this function in your root controller and use it any child scope like this:

$scope.resolve( 'path.to.any.object.in.scope')
Community
  • 1
  • 1
nesinervink
  • 333
  • 3
  • 13
  • See [AngularJS has `$scope.$eval`](https://stackoverflow.com/a/60553207/5535245) for another way to do it with AngularJS. – georgeawg Mar 05 '20 at 20:16
5

If you want a solution that can properly detect and report details of any issue with the path parsing, I wrote my own solution to this - library path-value.

const {resolveValue} = require('path-value');

resolveValue(someObject, 'part1.name'); //=> Part 1
resolveValue(someObject, 'part2.qty'); //=> 50
resolveValue(someObject, 'part3.0.name'); //=> Part 3A

Note that for indexes we use .0, and not [0], because parsing the latter adds a performance penalty, while .0 works directly in JavaScript, and is thus very fast.

However, full ES5 JavaScript syntax is also supported, it just needs to be tokenized first:

const {resolveValue, tokenizePath} = require('path-value');

const path = tokenizePath('part3[0].name'); //=> ['part3', '0', 'name']

resolveValue(someObject, path); //=> Part 3A
vitaly-t
  • 24,279
  • 15
  • 116
  • 138
4
/**
 * Access a deep value inside a object 
 * Works by passing a path like "foo.bar", also works with nested arrays like "foo[0][1].baz"
 * @author Victor B. https://gist.github.com/victornpb/4c7882c1b9d36292308e
 * Unit tests: http://jsfiddle.net/Victornpb/0u1qygrh/
 */
function getDeepVal(obj, path) {
    if (typeof obj === "undefined" || obj === null) return;
    path = path.split(/[\.\[\]\"\']{1,2}/);
    for (var i = 0, l = path.length; i < l; i++) {
        if (path[i] === "") continue;
        obj = obj[path[i]];
        if (typeof obj === "undefined" || obj === null) return;
    }
    return obj;
}

Works with

getDeepVal(obj,'foo.bar')
getDeepVal(obj,'foo.1.bar')
getDeepVal(obj,'foo[0].baz')
getDeepVal(obj,'foo[1][2]')
getDeepVal(obj,"foo['bar'].baz")
getDeepVal(obj,"foo['bar']['baz']")
getDeepVal(obj,"foo.bar.0.baz[1]['2']['w'].aaa[\"f\"].bb")
Vitim.us
  • 20,746
  • 15
  • 92
  • 109
3

I haven't yet found a package to do all of the operations with a string path, so I ended up writing my own quick little package which supports insert(), get() (with default return), set() and remove() operations.

You can use dot notation, brackets, number indices, string number properties, and keys with non-word characters. Simple usage below:

> var jsocrud = require('jsocrud');

...

// Get (Read) ---
> var obj = {
>     foo: [
>         {
>             'key w/ non-word chars': 'bar'
>         }
>     ]
> };
undefined

> jsocrud.get(obj, '.foo[0]["key w/ non-word chars"]');
'bar'

https://www.npmjs.com/package/jsocrud

https://github.com/vertical-knowledge/jsocrud

Kyle
  • 31
  • 4
3

Simple function, allowing for either a string or array path.

function get(obj, path) {
  if(typeof path === 'string') path = path.split('.');

  if(path.length === 0) return obj;
  return get(obj[path[0]], path.slice(1));
}

const obj = {a: {b: {c: 'foo'}}};

console.log(get(obj, 'a.b.c')); //foo

OR

console.log(get(obj, ['a', 'b', 'c'])); //foo
Ben
  • 2,962
  • 2
  • 20
  • 26
2

There is an npm module now for doing this: https://github.com/erictrinh/safe-access

Example usage:

var access = require('safe-access');
access(very, 'nested.property.and.array[0]');
caleb
  • 2,687
  • 30
  • 25
2

While reduce is good, I am surprised no one used forEach:

function valueForKeyPath(obj, path){
        const keys = path.split('.');
        keys.forEach((key)=> obj = obj[key]);
        return obj;
    };

Test

Flavien Volken
  • 19,196
  • 12
  • 100
  • 133
  • You are not even checking if obj[key] actually exists. It's unreliable. – Neithan Max Jul 22 '18 at 04:08
  • @CarlesAlcolea by default js will neither check if the key of an object exists: `a.b.c` will raise an exception if there is no property `b` in your object. If you need something silently dismissing the wrong keypath (which I do not recommend), you can still replace the forEach with this one ```keys.forEach((key)=> obj = (obj||{})[key]);``` – Flavien Volken Jul 23 '18 at 07:57
  • I run through it an object that was missing a curly brace, my bad :) – Neithan Max Jul 28 '18 at 23:59
2

I'm developing online-shop with React. I tried to change values in copied state object to update original state with it on submit. Examples above haven't worked for me, because most of them mutate structure of copied object. I found working example of the function for accessing and changing values of the deep nested object properties: https://lowrey.me/create-an-object-by-path-in-javascript-2/ Here it is:

const createPath = (obj, path, value = null) => {
  path = typeof path === 'string' ? path.split('.') : path;
  let current = obj;
  while (path.length > 1) {
    const [head, ...tail] = path;
    path = tail;
    if (current[head] === undefined) {
      current[head] = {};
    }
    current = current[head];
  }
  current[path[0]] = value;
  return obj;
};
Dm Mh
  • 820
  • 7
  • 9
2

You can use ramda library.

Learning ramda also helps you to work with immutable objects easily.


var obj = {
  a:{
    b: {
      c:[100,101,{
        d: 1000
      }]
    }
  }
};


var lens = R.lensPath('a.b.c.2.d'.split('.'));
var result = R.view(lens, obj);


https://codepen.io/ghominejad/pen/BayJZOQ

Ghominejad
  • 1,572
  • 16
  • 15
2

Based on Alnitak's answer.

I wrapped the polyfill in a check, and reduced the function to a single chained reduction.

if (Object.byPath === undefined) {
  Object.byPath = (obj, path) => path
    .replace(/\[(\w+)\]/g, '.$1')
    .replace(/^\./, '')
    .split(/\./g)
    .reduce((ref, key) => key in ref ? ref[key] : ref, obj)
}

const data = {
  foo: {
    bar: [{
      baz: 1
    }]
  }
}

console.log(Object.byPath(data, 'foo.bar[0].baz'))
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
2

This can be simplified by splitting the logic into three separate functions:

const isVal = a => a != null; // everything except undefined + null

const prop = prop => obj => {
    if (isVal(obj)) {
        const value = obj[prop];
        if (isVal(value)) return value;
        else return undefined;
    } else return undefined;
};

const path = paths => obj => {
    const pathList = typeof paths === 'string' ? paths.split('.') : paths;
    return pathList.reduce((value, key) => prop(key)(value), obj);
};

//usage:
const myObject = { foo: { bar: { baz: 'taco' } } };
const result = path('foo.bar')(myObject);
//results => { baz: 'taco' }

This variation supports:

  • passing an array or string argument
  • dealing with undefined values during invocation and execution
  • testing each function independently
  • using each function independently
Eonasdan
  • 7,563
  • 8
  • 55
  • 82
Alex Mckay
  • 3,463
  • 16
  • 27
2

DotObject = obj => new Proxy(obj, {
  get: function(o,k) {
    const m = k.match(/(.+?)\.(.+)/)
    return m ? this.get(o[m[1]], m[2]) : o[k]
  }
})

const test = DotObject({a: {b: {c: 'wow'}}})
console.log(test['a.b.c'])
Giulio
  • 469
  • 5
  • 15
1

Just had the same question recently and successfully used https://npmjs.org/package/tea-properties which also set nested object/arrays :

get:

var o = {
  prop: {
    arr: [
      {foo: 'bar'}
    ]
  }
};

var properties = require('tea-properties');
var value = properties.get(o, 'prop.arr[0].foo');

assert(value, 'bar'); // true

set:

var o = {};

var properties = require('tea-properties');
properties.set(o, 'prop.arr[0].foo', 'bar');

assert(o.prop.arr[0].foo, 'bar'); // true
abernier
  • 27,030
  • 20
  • 83
  • 114
1

Based on a previous answer, I have created a function that can also handle brackets. But no dots inside them due to the split.

function get(obj, str) {
  return str.split(/\.|\[/g).map(function(crumb) {
    return crumb.replace(/\]$/, '').trim().replace(/^(["'])((?:(?!\1)[^\\]|\\.)*?)\1$/, (match, quote, str) => str.replace(/\\(\\)?/g, "$1"));
  }).reduce(function(obj, prop) {
    return obj ? obj[prop] : undefined;
  }, obj);
}
Vincent
  • 2,342
  • 1
  • 17
  • 22
1

// (IE9+) Two steps

var pathString = "[0]['property'].others[3].next['final']";
var obj = [{
  property: {
    others: [1, 2, 3, {
      next: {
        final: "SUCCESS"
      }
    }]
  }
}];

// Turn string to path array
var pathArray = pathString
    .replace(/\[["']?([\w]+)["']?\]/g,".$1")
    .split(".")
    .splice(1);

// Add object prototype method
Object.prototype.path = function (path) {
  try {
    return [this].concat(path).reduce(function (f, l) {
      return f[l];
    });
  } catch (e) {
    console.error(e);
  }
};

// usage
console.log(obj.path(pathArray));
console.log(obj.path([0,"doesNotExist"]));
Oboo Cheng
  • 4,250
  • 3
  • 24
  • 29
1

Inspired by @webjay's answer: https://stackoverflow.com/a/46008856/4110122

I made this function which can you use it to Get/ Set/ Unset any value in object

function Object_Manager(obj, Path, value, Action) 
{
    try
    {
        if(Array.isArray(Path) == false)
        {
            Path = [Path];
        }

        let level = 0;
        var Return_Value;
        Path.reduce((a, b)=>{
            level++;
            if (level === Path.length)
            {
                if(Action === 'Set')
                {
                    a[b] = value;
                    return value;
                }
                else if(Action === 'Get')
                {
                    Return_Value = a[b];
                }
                else if(Action === 'Unset')
                {
                    delete a[b];
                }
            } 
            else 
            {
                return a[b];
            }
        }, obj);
        return Return_Value;
    }

    catch(err)
    {
        console.error(err);
        return obj;
    }
}

To use it:

 // Set
 Object_Manager(Obj,[Level1,Level2,Level3],New_Value, 'Set');

 // Get
 Object_Manager(Obj,[Level1,Level2,Level3],'', 'Get');

 // Unset
 Object_Manager(Obj,[Level1,Level2,Level3],'', 'Unset');
Mohamad Hamouday
  • 2,070
  • 23
  • 20
1

Extension of Mohamad Hamouday' Answer will fill in missing keys

function Object_Manager(obj, Path, value, Action, strict) 
{
    try
    {
        if(Array.isArray(Path) == false)
        {
            Path = [Path];
        }

        let level = 0;
        var Return_Value;
        Path.reduce((a, b)=>{
            console.log(level,':',a, '|||',b)
            if (!strict){
              if (!(b in a)) a[b] = {}
            }


            level++;
            if (level === Path.length)
            {
                if(Action === 'Set')
                {
                    a[b] = value;
                    return value;
                }
                else if(Action === 'Get')
                {
                    Return_Value = a[b];
                }
                else if(Action === 'Unset')
                {
                    delete a[b];
                }
            } 
            else 
            {
                return a[b];
            }
        }, obj);
        return Return_Value;
    }

    catch(err)
    {
        console.error(err);
        return obj;
    }
}

Example


obja = {
  "a": {
    "b":"nom"
  }
}

// Set
path = "c.b" // Path does not exist
Object_Manager(obja,path.split('.'), 'test_new_val', 'Set', false);

// Expected Output: Object { a: Object { b: "nom" }, c: Object { b: "test_new_value" } }

sam
  • 1,005
  • 1
  • 11
  • 24
1

AngularJS has $scope.$eval

With AngularJS, one can use the $scope.$eval method to access nested objects:

$scope.someObject = someObject;
console.log( $scope.$eval("someObject.part3[0].name") ); //Part 3A

For more information, see

The DEMO

angular.module("app",[])
.run(function($rootScope) {
     $rootScope.someObject = {
         'part2' : {
              'name': 'Part 2',
              'size': '15',
              'qty' : '60'
         },
         'part3' : [{
              'name': 'Part 3A',
              'size': '10',
              'qty' : '20'
         },{
              name: 'Part 3B'           
         }]
     };
     console.log(
         "part3[0].name =",
         $rootScope.$eval("someObject.part3[0].name")
    );
})
<script src="//unpkg.com/angular/angular.js"></script>
<body ng-app="app"
</body>
Community
  • 1
  • 1
georgeawg
  • 48,608
  • 13
  • 72
  • 95
1

Note that the following will not work for all valid unicode property names (but neither will any of the other answers as far as I can tell).

const PATTERN = /[\^|\[|\.]([$|\w]+)/gu

function propValue(o, s) {
    const names = []
    for(let [, name] of [...s.matchAll(PATTERN)]) 
        names.push(name)
    return names.reduce((p, propName) => {
        if(!p.hasOwnProperty(propName)) 
            throw 'invalid property name'
        return p[propName]
    }, o)
}

let path = 'myObject.1._property2[0][0].$property3'
let o = {
    1: {
        _property2: [
            [{
                $property3: 'Hello World'
            }]
        ]
    }
}
console.log(propValue(o, path)) // 'Hello World'
Ben Aston
  • 53,718
  • 65
  • 205
  • 331
1

If you need to access different nested key without knowing it at coding time (it will be trivial to address them) you can use the array notation accessor:

var part1name = someObject['part1']['name'];
var part2quantity = someObject['part2']['qty'];
var part3name1 =  someObject['part3'][0]['name'];

They are equivalent to the dot notation accessor and may vary at runtime, for example:

var part = 'part1';
var property = 'name';

var part1name = someObject[part][property];

is equivalent to

var part1name = someObject['part1']['name'];

or

var part1name = someObject.part1.name;

I hope this address your question...

EDIT

I won't use a string to mantain a sort of xpath query to access an object value. As you have to call a function to parse the query and retrieve the value I would follow another path (not :

var part1name = function(){ return this.part1.name; }
var part2quantity = function() { return this['part2']['qty']; }
var part3name1 =  function() { return this.part3[0]['name'];}

// usage: part1name.apply(someObject);

or, if you are uneasy with the apply method

var part1name = function(obj){ return obj.part1.name; }
var part2quantity = function(obj) { return obj['part2']['qty']; }
var part3name1 =  function(obj) { return obj.part3[0]['name'];}

// usage: part1name(someObject);

The functions are shorter, clearer, the interpreter check them for you for syntax errors and so on.

By the way, I feel that a simple assignment made at right time will be sufficent...

Eineki
  • 14,773
  • 6
  • 50
  • 59
  • Interesting. But in my case, the someObject is initialize yet when I assign value to part1name. I only know the structure. That is why I use string to describe the structure. And hoping to be able to use it to query my data from someObject. Thanks for sharing your thought. :) – Komaruloh Jun 27 '11 at 10:47
  • @Komaruloh : I think you would write that the object is NOT initialized yet when you create your variables. By the way I don't get the point, why can't you do the assignment at appropriate time? – Eineki Jun 27 '11 at 11:24
  • Sorry about not mentioning that someObject is not initialized yet. As for the reason, someObject is fetch via web service. And I want to have an array of header which consist of part1name, part2qty, etc. So that I could just loop through the header array and get the value I wanted based on part1name value as the 'key'/path to someObject. – Komaruloh Jun 27 '11 at 11:47
1

Using object-scan this becomes a one liner. However more importantly this solution considers performance:

  • input traversed once during search (even if multiple keys are queried)
  • parsing only happens once on init (in case multiple objects are queried)
  • allow for extended syntax using *

// const objectScan = require('object-scan');

const someObject = { part1: { name: 'Part 1', size: '20', qty: '50' }, part2: { name: 'Part 2', size: '15', qty: '60' }, part3: [{ name: 'Part 3A', size: '10', qty: '20' }, { name: 'Part 3B', size: '5', qty: '20' }, { name: 'Part 3C', size: '7.5', qty: '20' }] };

const get = (haystack, needle) => objectScan([needle], { rtn: 'value', abort: true })(haystack);

console.log(get(someObject, 'part1.name'));
// => Part 1
console.log(get(someObject, 'part2.qty'));
// => 60
console.log(get(someObject, 'part3[0].name'));
// => Part 3A

const getAll = (haystack, ...needles) => objectScan(needles, { reverse: false, rtn: 'entry', joined: true })(haystack);

console.log(getAll(someObject, 'part1.name', 'part2.qty', 'part3[0].name'));
/* =>
[ [ 'part1.name', 'Part 1' ],
  [ 'part2.qty', '60' ],
  [ 'part3[0].name', 'Part 3A' ] ]
 */

console.log(getAll(someObject, 'part1.*'));
/* =>
[ [ 'part1.name', 'Part 1' ],
  [ 'part1.size', '20' ],
  [ 'part1.qty', '50' ] ]
 */
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan@13.8.0"></script>

Disclaimer: I'm the author of object-scan

vincent
  • 1,953
  • 3
  • 18
  • 24
1

My solution is based on that given by @AdrianoSpadoni and addresses a need to clone the object

function generateData(object: any, path: string, value: any): object {
  const clone = JSON.parse(JSON.stringify(object));
  path
    .split(".")
    .reduce(
    (o, p, i) => (o[p] = path.split(".").length === ++i ? value : o[p] || {}),
  clone
);
  return clone;
}
timebandit
  • 794
  • 2
  • 11
  • 26
1

I've looked on all the other answers, decided to add improvements into more readable code:

function getObjectValByString(obj, str) {
if (typeof obj === "string") return obj;

const fields = str.split(".");

return getObjectValByString(obj[fields[0]], fields.slice(1).join("."));}

heres a code snippet:

let someObject = {
    partner: {
        id: "AIM",
        person: {
            name: "ANT",
            an: { name: "ESM" },
        },
    },
};

function getObjectValByString(obj, str) {
    if (typeof obj === "string") return obj;

    const fields = str.split(".");

    return getObjectValByString(obj[fields[0]], fields.slice(1).join("."));
}

const result = getObjectValByString(someObject, "partner.person.an.name");
console.log({
    result,
});
Re9iNee
  • 412
  • 5
  • 15
0

What about this solution:

setJsonValue: function (json, field, val) {
  if (field !== undefined){
    try {
      eval("json." + field + " = val");
    }
    catch(e){
      ;
    }
  }  
}

And this one, for getting:

getJsonValue: function (json, field){
  var value = undefined;
  if (field !== undefined) {
    try {
      eval("value = json." + field);
    } 
    catch(e){
      ;
    }
  }
  return value;
};

Probably some will consider them unsafe, but they must be much faster then, parsing the string.

Jonan Gueorguiev
  • 1,146
  • 12
  • 20
  • 1
    +1, This is a totally valid answer when the performance matters and you can create the getter functions ahead of time and guarantee that the strings are safe. Although I would use `new Function` which is a slightly safer as it can't access variables outside of it's scope. We are talking 10x faster per lookup. – Dominic Oct 13 '20 at 17:10
0

The solutions here are just for accessing the deeply nested keys. I needed one for accessing, adding, modifying and deleting the keys. This is what I came up with:

var deepAccessObject = function(object, path_to_key, type_of_function, value){
    switch(type_of_function){
        //Add key/modify key
        case 0: 
            if(path_to_key.length === 1){
                if(value)
                    object[path_to_key[0]] = value;
                return object[path_to_key[0]];
            }else{
                if(object[path_to_key[0]])
                    return deepAccessObject(object[path_to_key[0]], path_to_key.slice(1), type_of_function, value);
                else
                    object[path_to_key[0]] = {};
            }
            break;
        //delete key
        case 1:
            if(path_to_key.length === 1){
                delete object[path_to_key[0]];
                return true;
            }else{
                if(object[path_to_key[0]])
                    return deepAccessObject(object[path_to_key[0]], path_to_key.slice(1), type_of_function, value);
                else
                    return false;
            }
            break;
        default:
            console.log("Wrong type of function");
    }
};
  • path_to_key: path in an array. You can replace it by your string_path.split(".").
  • type_of_function: 0 for accessing(dont pass any value to value), 0 for add and modify. 1 for delete.
ayushgp
  • 4,891
  • 8
  • 40
  • 75
0

Building off of Alnitak's answer:

if(!Object.prototype.byString){
  //NEW byString which can update values
Object.prototype.byString = function(s, v, o) {
  var _o = o || this;
      s = s.replace(/\[(\w+)\]/g, '.$1'); // CONVERT INDEXES TO PROPERTIES
      s = s.replace(/^\./, ''); // STRIP A LEADING DOT
      var a = s.split('.'); //ARRAY OF STRINGS SPLIT BY '.'
      for (var i = 0; i < a.length; ++i) {//LOOP OVER ARRAY OF STRINGS
          var k = a[i];
          if (k in _o) {//LOOP THROUGH OBJECT KEYS
              if(_o.hasOwnProperty(k)){//USE ONLY KEYS WE CREATED
                if(v !== undefined){//IF WE HAVE A NEW VALUE PARAM
                  if(i === a.length -1){//IF IT'S THE LAST IN THE ARRAY
                    _o[k] = v;
                  }
                }
                _o = _o[k];//NO NEW VALUE SO JUST RETURN THE CURRENT VALUE
              }
          } else {
              return;
          }
      }
      return _o;
  };

}

This allows you to set a value as well!

I've created an npm package and github with this as well

Tamb
  • 748
  • 11
  • 20
0

Instead of a string an array can be used adressing nested objects and arrays e.g.: ["my_field", "another_field", 0, "last_field", 10]

Here is an example that would change a field based on this array representation. I am using something like that in react.js for controlled input fields that change the state of nested structures.

let state = {
        test: "test_value",
        nested: {
            level1: "level1 value"
        },
        arr: [1, 2, 3],
        nested_arr: {
            arr: ["buh", "bah", "foo"]
        }
    }

function handleChange(value, fields) {
    let update_field = state;
    for(var i = 0; i < fields.length - 1; i++){
        update_field = update_field[fields[i]];
    }
    update_field[fields[fields.length-1]] = value;
}

handleChange("update", ["test"]);
handleChange("update_nested", ["nested","level1"]);
handleChange(100, ["arr",0]);
handleChange('changed_foo', ["nested_arr", "arr", 3]);
console.log(state);
Jodo
  • 4,515
  • 6
  • 38
  • 50
0

Working with Underscore's property or propertyOf:

var test = {
  foo: {
    bar: {
      baz: 'hello'
    }
  }
}
var string = 'foo.bar.baz';


// document.write(_.propertyOf(test)(string.split('.')))

document.write(_.property(string.split('.'))(test));
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>

Good Luck...

Aakash
  • 21,375
  • 7
  • 100
  • 81
0

React example - Using lodash

This may not be the most efficient way, from a performance perspective, but if your app is some monolith it sure as heck will save you some time. Especially, when you are tightly coupling your state data format to an API back-end.

   import set from "lodash/set";  // More efficient import

    class UserProfile extends Component {

      constructor(props){
        super(props);

        this.state = {
          user: {
            account: {
              id: "",
              email: "",
              first_name: ""
            }
          }
        }
      }

       /**
       * Updates the state based on the form input
       * 
       * @param {FormUpdate} event 
       */
      userAccountFormHook(event) {
        // https://lodash.com/docs#get
        // https://lodash.com/docs#set
        const { name, value } = event.target;
        let current_state = this.state
        set(current_state, name, value)  // Magic happens here
        this.setState(current_state);
      }

    render() {
        return (
          <CustomFormInput
            label: "First Name"
            type: "text"
            placeholder: "First Name"
            name: "user.account.first_name"
            onChange: {this.userAccountFormHook}
            value: {this.state.user.account.first_name}

          />
      )
  }
}
ThatGuyRob
  • 126
  • 1
  • 12
0

Starting from @Alnitak answer I built this source, which downloads an actual .JSON file and processes it, printing to console explanatory strings for each step, and more details in case of wrong key passed:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
  <script>
function retrieveURL(url) {
        var client = new XMLHttpRequest();
        prefix = "https://cors-anywhere.herokuapp.com/"
        client.open('GET', prefix + url);
        client.responseType = 'text';
        client.onload = function() {
            response = client.response; // Load remote response.
            console.log("Response received.");
            parsedJSON  = JSON.parse(response);
            console.log(parsedJSON);
            console.log(JSONitemByPath(parsedJSON,"geometry[6].obs[3].latituade"));
            return response;
        };
        try {
            client.send();
        } catch(e) {
            console.log("NETWORK ERROR!");
            console.log(e);
        }
}



function JSONitemByPath(o, s) {
    structure = "";
    originalString = s;
    console.log("Received string: ", s);
    s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
    console.log("Converted to   : ", s);
    s = s.replace(/^\./, '');           // strip a leading dot
    var a = s.split('.');

    console.log("Single keys to parse: ",a);

    for (var i = 0, n = a.length; i < n; ++i) {
        var k = a[i];
        if (k in o) {
            o = o[k];
            console.log("object." + structure +  a[i], o);
            structure +=  a[i] + ".";
        } else {
            console.log("ERROR: wrong path passed: ", originalString);
            console.log("       Last working level: ", structure.substr(0,structure.length-1));
            console.log("       Contents: ", o);
            console.log("       Available/passed key: ");
            Object.keys(o).forEach((prop)=> console.log("       "+prop +"/" + k));
            return;
        }
    }
    return o;
}


function main() {
    rawJSON = retrieveURL("http://haya2now.jp/data/data.json");
}

</script>
  </head>
  <body onload="main()">
  </body>
</html>

Output example:

Response received.
json-querier.html:17 {geometry: Array(7), error: Array(0), status: {…}}
json-querier.html:34 Received string:  geometry[6].obs[3].latituade
json-querier.html:36 Converted to   :  geometry.6.obs.3.latituade
json-querier.html:40 Single keys to parse:  (5) ["geometry", "6", "obs", "3", "latituade"]
json-querier.html:46 object.geometry (7) [{…}, {…}, {…}, {…}, {…}, {…}, {…}]
json-querier.html:46 object.geometry.6 {hayabusa2: {…}, earth: {…}, obs: Array(6), TT: 2458816.04973593, ryugu: {…}, …}
json-querier.html:46 object.geometry.6.obs (6) [{…}, {…}, {…}, {…}, {…}, {…}]
json-querier.html:46 object.geometry.6.obs.3 {longitude: 148.98, hayabusa2: {…}, sun: {…}, name: "DSS-43", latitude: -35.4, …}
json-querier.html:49 ERROR: wrong path passed:  geometry[6].obs[3].latituade
json-querier.html:50        Last working level:  geometry.6.obs.3
json-querier.html:51        Contents:  {longitude: 148.98, hayabusa2: {…}, sun: {…}, name: "DSS-43", latitude: -35.4, …}
json-querier.html:52        Available/passed key: 
json-querier.html:53        longitude/latituade
json-querier.html:53        hayabusa2/latituade
json-querier.html:53        sun/latituade
json-querier.html:53        name/latituade
json-querier.html:53        latitude/latituade
json-querier.html:53        altitude/latituade
json-querier.html:18 undefined
jumpjack
  • 841
  • 1
  • 11
  • 17
0

After reading through the other answers, I believe the most performant and concise way of replacing _.get() and _.set() is by making a module with the following:

let rgxBracketToDot;

export function sanitizePath (path) {
    path = path || [];
    return Array.isArray(path) ? path : path.replace(rgxBracketToDot || (rgxBracketToDot = /\[(\w+)\]/g), '.$1').split('.');
}

export function get (obj, path) {
    if (!obj || typeof obj !== 'object') {
        return;
    }
    return sanitizePath(path).reduce((acc, val) => acc && acc[val], obj);
}

export function set (obj, path, value) {
    const [current,...rest] = sanitizePath(path);
    rest.length >= 1 ? set(obj[current] = obj[current] || {}, rest, value) : obj[current]= value;
    return obj;
}

To maintain full compatibility with lodash, two additional .replace() calls could be included in sanitizePath() to remove leading and trailing dots (.):

path = path.replace(/^\./, '');
path = path.replace(/\.$/, '');

This should be done in a similar way to rgxBracketToDot so that the regex is only set once.

If you have full control over the path arguments, you could also make the code more performant by only using array paths and removing sanitizeString() altogether.

adjwilli
  • 9,658
  • 4
  • 35
  • 29
0

Another solution:

export function getNestedFieldByStringKey(obj, path) {
  const squareBracketsRgx = /\[(\w|'|")*\]/g
  const squareBracketsToDot = (sb: string) => `.${sb.replace(/\[|\]|'|"/g, '')}`
  const parts = path
    .replace(squareBracketsRgx, squareBracketsToDot)
    .split('.')

  return parts.reduce((o, part) => o?.[part], obj)
}

And these are some tests:

describe('getNestedFieldByStringKey', () => {
    it('should return the nested field using "." as separator', () => {
      const input = {
        some: {
          example: {
            nested: true
          }
        }
      }

      expect(getNestedFieldByStringKey(input, 'some.example.nested')).toBe(true)
    })

    it('should return the nested field using "." and "[]" as separator', () => {
      const input = {
        some: {
          example: {
            nested: [{ test: true }]
          }
        }
      }

      expect(getNestedFieldByStringKey(input, 'some["example"].nested[0].test')).toBe(true)
    })

    it('should return undefined if does not exist', () => {
      const input = {}

      expect(getNestedFieldByStringKey(input, 'some["example"].nested[0].test')).toBe(undefined)
    })
})
Aral Roca
  • 5,442
  • 8
  • 47
  • 78