62

Suppose we are only given

var obj = {};
var propName = "foo.bar.foobar";

How can we set the property obj.foo.bar.foobar to a certain value (say "hello world")? So I want to achieve this, while we only have the property name in a string:

obj.foo.bar.foobar = "hello world";
user2864740
  • 60,010
  • 15
  • 145
  • 220
chtenb
  • 14,924
  • 14
  • 78
  • 116
  • 3
    Duplicate of http://stackoverflow.com/questions/6842795/dynamic-deep-setting-for-a-javascript-object – Cerbrus Dec 05 '12 at 09:14
  • 1
    This might help someone who is trying to understand the answer to this question...http://stackoverflow.com/questions/39060905/how-recursion-takes-place-in-this-code-snippet – Ankur Marwaha Aug 21 '16 at 12:26

16 Answers16

92
function assign(obj, prop, value) {
    if (typeof prop === "string")
        prop = prop.split(".");

    if (prop.length > 1) {
        var e = prop.shift();
        assign(obj[e] =
                 Object.prototype.toString.call(obj[e]) === "[object Object]"
                 ? obj[e]
                 : {},
               prop,
               value);
    } else
        obj[prop[0]] = value;
}

var obj = {},
    propName = "foo.bar.foobar";

assign(obj, propName, "Value");
VisioN
  • 143,310
  • 32
  • 282
  • 281
  • 1
    Yep, this one also appears to work when the path doesn't exist yet. – chtenb Dec 05 '12 at 09:27
  • Why do you check for the typeof prop? You continue the function flow anyways. – Stephan Bönnemann-Walenta Dec 05 '12 at 09:39
  • @StephanBönnemann, if it's not a string, we're in the iteration when we need to set the property with `obj[prop[0]] = value;` – Cerbrus Dec 05 '12 at 09:42
  • @StephanBönnemann Not sure what you mean. This is added to make the solution univeral, so that we can pass either array or string as `prop`. – VisioN Dec 05 '12 at 09:43
  • @VisioN okay, made a jsperf testcase and it turns out you are faster anyways: http://jsperf.com/nested-object-assignment edit: turns out chrome is faster your way, firefox and safari are faster my way. – Stephan Bönnemann-Walenta Dec 05 '12 at 09:54
  • @VisioN: The recursion function appears to empty existing objects in the specified path. (try setting `foo.bar.foobar` and `foo.bar2.foobar2`) I edited my answer to fix that. I suggest you do the same. – Cerbrus Dec 05 '12 at 10:12
  • @Cerbrus Nice catch, thanks! I have improved the solution a bit. – VisioN Dec 05 '12 at 10:20
  • @Cerbrus I updated the test case and tried to go deeper than 3 properties, too. Turns out the deeper you go, the more efficient my answer gets: http://jsperf.com/nested-object-assignment – Stephan Bönnemann-Walenta Dec 05 '12 at 10:25
  • @StephanBönnemann: I'd suggest adding separate testcases for the "shallow" and "deep" tests. ("Add code snippet" next to the "Save test case" button) – Cerbrus Dec 05 '12 at 10:35
  • if `obj= {"foo":3}`and you try `assign(obj,"foo.bar.foobar","Value")`? if `obj= {"foo":false}`and you try `assign(obj,"foo.bar.foobar","Value")`? – FrancescoMM Dec 05 '12 at 10:55
  • This is magic , I modified it and am using it in my library `setDeep(obj, prop, value, returnObj) { if (is.String(prop)) prop = prop.split("."); if (prop.length > 1) { let e = prop.shift(); Craft.setDeep(obj[e] = is.Object(obj[e]) ? obj[e] : {}, prop, value); } else obj[prop[0]] = value; if (returnObj === true) return obj; } ` – Saul does Code Dec 26 '15 at 17:39
  • Can someone explain how the recursion is working, the function is being called with a false value but at the time of returning it is being called with a true value ? In short how the recursion is taking place ? – Ankur Marwaha Aug 18 '16 at 13:54
  • 1
    This doesn't work with arrays. template[0].item[0].format.color – Demodave Aug 07 '18 at 18:59
  • can you help me to make this one in C#? I have a JSON path and wanted to generate a JSON object based on that path – Umair Anwaar Mar 09 '21 at 13:49
15

I know it's an old one, but I see only custom functions in answers.
If you don't mind using a library, look at lodash _.set and _.get function.

Dariusz Filipiak
  • 2,858
  • 5
  • 28
  • 39
12

Since this question appears to be answered by incorrect answers, I'll just refer to the correct answer from a similar question

function setDeepValue(obj, value, path) {
    if (typeof path === "string") {
        var path = path.split('.');
    }

    if(path.length > 1){
        var p=path.shift();
        if(obj[p]==null || typeof obj[p]!== 'object'){
             obj[p] = {};
        }
        setDeepValue(obj[p], value, path);
    }else{
        obj[path[0]] = value;
    }
}

Use:

var obj = {};
setDeepValue(obj, 'Hello World', 'foo.bar.foobar');
Community
  • 1
  • 1
Cerbrus
  • 70,800
  • 18
  • 132
  • 147
  • 3
    Hm. Your answer looks like an exact copy of mine :) – VisioN Dec 05 '12 at 09:41
  • It looks like I left out a `=` in the `typeof`. But frankly, `typeof` is the only way to make sure you're not trying to split a object, one of the things I ran into. For the rest, it's simple recursion. – Cerbrus Dec 05 '12 at 09:44
  • 1
    What if I call it twice with `foo.bar.foobar` and `foo.bar2.foobar2`? – FrancescoMM Dec 05 '12 at 10:05
  • In that case, it did reset `obj.foo`, when setting `foo.bar2.foobar2`. Edited the code. Whoops, didn't see your suggested edit, fixing. – Cerbrus Dec 05 '12 at 10:10
  • @Cerbrus one more thing. If obj.foo.bar is assigned a value and then I try to setDeepValue foo.bar.foobar it it fails with an error (unless the value was boolean `false`). Probably the most safe way is to test if obj[p] is an object. If not an object, replace it with {}. As null is an object, the correct test should be `if(obj[p]!=null && typeof obj[p]=== 'object')` – FrancescoMM Dec 05 '12 at 10:31
  • Actually `if(obj[p]==null || typeof obj[p]!== 'object') obj[p]={};` should do the trick – FrancescoMM Dec 05 '12 at 11:56
  • Is there really a difference? – Cerbrus Dec 05 '12 at 11:59
  • Not ìmportant`, but.. Yes, if at start `obj= {"foo":3}` and you try setDeepValue(obj,"Value","foo.bar.foobar") you will see the difference. At some iteration obj will be 3, and you will try to add elements to it as if it was an object, 3['bar']={}. While if you test for `if(obj[p]==null || typeof obj[p]!== 'object') obj[p]={};` it will catch the non-object value 3, and replace it with {}. In @Vision's answer he uses `Object.prototype.toString.call(obj[e]) === "[object Object]"` that works the same and excludes arrays and other classes too (wonder if that's too strict). – FrancescoMM Dec 06 '12 at 09:48
  • @bla: It _is_ recursive... `setDeepValue(obj[p], value, path);` – Cerbrus Aug 20 '14 at 17:05
5

edit: I've created a jsPerf.com testcase to compare the accepted answer with my version. Turns out that my version is faster, especially when you go very deep.

http://jsfiddle.net/9YMm8/

var nestedObjectAssignmentFor = function(obj, propString, value) {
    var propNames = propString.split('.'),
        propLength = propNames.length-1,
        tmpObj = obj;

    for (var i = 0; i <= propLength ; i++) {
        tmpObj = tmpObj[propNames[i]] = i !== propLength ?  {} : value;  
    }
    return obj;
}

var obj = nestedObjectAssignment({},"foo.bar.foobar","hello world");

  • [This appears to be slightly slower in Chrome](http://jsperf.com/object-for-vs-rec) than the proposed recursion function. **However, It's significantly faster in IE / FF**. (Still, Chrome has the most iterations / second, [V8](http://code.google.com/p/v8/) being a beast with JavaScript execution) – Cerbrus Dec 05 '12 at 09:59
  • @Cerbrus appears that it depends on the browser, created the testcase myself http://jsperf.com/nested-object-assignment – Stephan Bönnemann-Walenta Dec 05 '12 at 10:02
  • 1
    Code performance testing 101: Don't initialize your functions for every iteration – Cerbrus Dec 05 '12 at 10:03
  • Only works with empty first argument objects, otherwise it overwrite the original object data. – art Sep 20 '16 at 17:27
4

All solutions overid any of the original data when setting so I have tweaked with the following, made it into a single object too:

 var obj = {}
 nestObject.set(obj, "a.b", "foo"); 
 nestObject.get(obj, "a.b"); // returns foo     

 var nestedObject = {
     set: function(obj, propString, value) {
         var propNames = propString.split('.'),
             propLength = propNames.length-1,
             tmpObj = obj;
         for (var i = 0; i <= propLength ; i++) {
             if (i === propLength){
                 if(tmpObj[propNames[i]]){
                     tmpObj[propNames[i]] = value;
                 }else{
                     tmpObj[propNames[i]] = value;
                 }
             }else{
                 if(tmpObj[propNames[i]]){
                     tmpObj = tmpObj[propNames[i]];
                 }else{
                     tmpObj = tmpObj[propNames[i]] = {};
                 }
             }
         }
         return obj;
     },
     get: function(obj, propString){
         var propNames = propString.split('.'),
             propLength = propNames.length-1,
             tmpObj = obj;
         for (var i = 0; i <= propLength ; i++) {
             if(tmpObj[propNames[i]]){
                 tmpObj = tmpObj[propNames[i]];
             }else{
                 break;
             }
         }
         return tmpObj;
     }
 };

Can also change functions to be an Oject.prototype method changing obj param to this:

Object.prototype = { setNested = function(){ ... }, getNested = function(){ ... } } 

{}.setNested('a.c','foo') 
Labithiotis
  • 3,519
  • 7
  • 27
  • 47
3

Here is a get and set function i just compiled from a couple of threads + some custom code.

It will also create keys that don't exist on set.

function setValue(object, path, value) {
    var a = path.split('.');
    var o = object;
    for (var i = 0; i < a.length - 1; i++) {
        var n = a[i];
        if (n in o) {
            o = o[n];
        } else {
            o[n] = {};
            o = o[n];
        }
    }
    o[a[a.length - 1]] = value;
}

function getValue(object, path) {
    var o = object;
    path = path.replace(/\[(\w+)\]/g, '.$1');
    path = path.replace(/^\./, '');
    var a = path.split('.');
    while (a.length) {
        var n = a.shift();
        if (n in o) {
            o = o[n];
        } else {
            return;
        }
    }
    return o;
}
chtenb
  • 14,924
  • 14
  • 78
  • 116
Dieter Gribnitz
  • 5,062
  • 2
  • 41
  • 38
  • 1
    I like this one because it does array index also, e.g. "property[3]". However, to get setValue to work like that we must also add "path = path.replace(/\[(\w+)\]/g, '.$1');" to the setValue method. – Etherman Dec 03 '18 at 18:25
3

Here's one that returns the updated object

function deepUpdate(value, path, tree, branch = tree) {
  const last = path.length === 1;
  branch[path[0]] = last ? value : branch[path[0]];
  return last ? tree : deepUpdate(value, path.slice(1), tree, branch[path[0]]);
}

const path = 'cat.dog';
const updated = deepUpdate('a', path.split('.'), {cat: {dog: null}})
// => { cat: {dog: 'a'} }
Daniel Lizik
  • 3,058
  • 2
  • 20
  • 42
3

Here is a simple function to do that using reference.

    function setValueByPath (obj, path, value) {
        var ref = obj;

        path.split('.').forEach(function (key, index, arr) {
            ref = ref[key] = index === arr.length - 1 ? value : {};
        });

        return obj;
    }
3

You could split the path and make a check if the following element exist. If not assign an object to the new property.

Return then the value of the property.

At the end assign the value.

function setValue(object, path, value) {
    var fullPath = path.split('.'),
        way = fullPath.slice(),
        last = way.pop();

    way.reduce(function (r, a) {
        return r[a] = r[a] || {};
    }, object)[last] = value;
}

var object = {},
    propName = 'foo.bar.foobar',
    value = 'hello world';

setValue(object, propName, value);
console.log(object);
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
3

A very straightforward one.

This implementation should be very performant. It avoids recursions, and function calls, while maintaining simplicity.

/**
 * Set the value of a deep property, creating new objects as necessary.
 * @param {Object} obj The object to set the value on.
 * @param {String|String[]} path The property to set.
 * @param {*} value The value to set.
 * @return {Object} The object at the end of the path.
 * @author github.com/victornpb
 * @see https://stackoverflow.com/a/46060952/938822
 * @example
 * setDeep(obj, 'foo.bar.baz', 'quux');
 */
function setDeep(obj, path, value) {
    const props = typeof path === 'string' ? path.split('.') : path;
    for (var i = 0, n = props.length - 1; i < n; ++i) {
        obj = obj[props[i]] = obj[props[i]] || {};
    }
    obj[props[i]] = value;
    return obj;
}
  
  

/*********************** EXAMPLE ***********************/

const obj = {
    hello : 'world',
};

setDeep(obj, 'root', true);
setDeep(obj, 'foo.bar.baz', 1);
setDeep(obj, ['foo','quux'], '');

console.log(obj);
// ⬇︎ Click "Run" below to see output
Vitim.us
  • 20,746
  • 15
  • 92
  • 109
  • Should support setting of deep arrays: `setDeep(obj, 'foo.bar.baz[0]', 1);` – vsync Aug 08 '22 at 10:11
  • @vsync I sets values inside arrays, but if the array doesn't exist it assumes object creation, and threats the index as a key. – Vitim.us Sep 13 '22 at 21:12
1

I was looking for an answer that does not overwrite existing values and was easily readable and was able to come up with this. Leaving this here in case it helps others with the same needs

function setValueAtObjectPath(obj, pathString, newValue) {
  // create an array (pathComponents) of the period-separated path components from pathString
  var pathComponents = pathString.split('.');
  // create a object (tmpObj) that references the memory of obj
  var tmpObj = obj;

  for (var i = 0; i < pathComponents.length; i++) {
    // if not on the last path component, then set the tmpObj as the value at this pathComponent
    if (i !== pathComponents.length-1) {
      // set tmpObj[pathComponents[i]] equal to an object of it's own value
      tmpObj[pathComponents[i]] = {...tmpObj[pathComponents[i]]}
      // set tmpObj to reference tmpObj[pathComponents[i]]
      tmpObj = tmpObj[pathComponents[i]]
    // else (IS the last path component), then set the value at this pathComponent equal to newValue 
    } else {
      // set tmpObj[pathComponents[i]] equal to newValue
      tmpObj[pathComponents[i]] = newValue
    }
  }
  // return your object
  return obj
}
Rbar
  • 3,740
  • 9
  • 39
  • 69
1

Same as Rbar's answers, very useful when you're working with redux reducers. I use lodash clone instead of spread operator to support arrays too:

export function cloneAndPatch(obj, path, newValue, separator='.') {
    let stack = Array.isArray(path) ? path : path.split(separator);
    let newObj = _.clone(obj);

    obj = newObj;

    while (stack.length > 1) {
        let property = stack.shift();
        let sub = _.clone(obj[property]);

        obj[property] = sub;
        obj = sub;
    }

    obj[stack.shift()] = newValue;

    return newObj;
}
makeroo
  • 509
  • 8
  • 10
1
Object.getPath = 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;
};

Object.setPath = function(o, p, v) {
    var a = p.split('.');
    var o = o;
    for (var i = 0; i < a.length - 1; i++) {
        if (a[i].indexOf('[') === -1) {
            var n = a[i];
            if (n in o) {
                o = o[n];
            } else {
                o[n] = {};
                o = o[n];
            }
        } else {
            // Not totaly optimised
            var ix = a[i].match(/\[.*?\]/g)[0];
            var n = a[i].replace(ix, '');
            o = o[n][ix.substr(1,ix.length-2)]
        }
    }

    if (a[a.length - 1].indexOf('[') === -1) {
        o[a[a.length - 1]] = v;
    } else {
        var ix = a[a.length - 1].match(/\[.*?\]/g)[0];
        var n = a[a.length - 1].replace(ix, '');
        o[n][ix.substr(1,ix.length-2)] = v;
    }
};
ip.
  • 3,306
  • 6
  • 32
  • 42
1

Here's a simple method that uses a scoped Object that recursively set's the correct prop by path.

function setObjectValueByPath(pathScope, value, obj) {
  const pathStrings = pathScope.split('/');
  obj[pathStrings[0]] = pathStrings.length > 1 ?
    setObjectValueByPath(
      pathStrings.splice(1, pathStrings.length).join('/'),
      value,
      obj[pathStrings[0]]
    ) :
    value;
  return obj;
}
Max Sandelin
  • 130
  • 6
0

How about a simple and short one?

Object.assign(this.origin, { [propName]: value })

0

You can use reduce : (you can test it by copy/paste on browser console)

const setValueOf = (obj, value, ...path) => {
    path.reduce((o, level, idx) => {
        if(idx === path.length -1) { o[level] = value }; // on last change the value of the prop
        return o && o[level]; // return the prop
    }, obj);
};

Example

let objExmp = {a: 'a', b: {b1: 'b1', b2: 'b2', b3: { b3_3 : 'default_value' } }};

setValueOf(objExmp, 'new_value' , 'b', 'b3', 'b3_3');
console.log('objExmp', objExmp); // prop changed to 'new_value'

You can split the string path by '.' and spread like :

setValueOf(objExmp, 'new_value' , ...'b.b3.b3_3'.split('.'));
Tkin R.
  • 61
  • 1
  • 3