0

I am trying to recursively build an object with a tree of properties based on a MongoDB-ish selector "top.middle.bottom". There are some underscorejs helpers as well:

function setNestedPropertyValue(obj, fields, val) {
  if (fields.indexOf(".") === -1) { 
    // On last property, set the value
    obj[fields] = val; 
    return obj; // Recurse back up
  } else {
    var oneLevelLess = _.first(fields.split("."));
    var remainingLevels = _.rest(fields.split(".")).join(".");
    // There are more property levels remaining, set a sub with a recursive call
    obj[oneLevelLess] = setNestedPropertyValue( {}, remainingLevels, val);
  }
}

setNestedPropertyValue({}, "grandpaprop.papaprop.babyprop", 1);

Desired:

{ 
  grandpaprop: {
    papaprop: {
      babyprop: 1
    }
  }
}

Outcome:

undefined

Helps and hints would be appreciated.

4
  • You're not returning obj in the second branch. That said, why are you using recursion in the first place? Commented May 3, 2013 at 9:00
  • 1
    possible duplicate of How can I programmatically add to a variably-nested object? Commented May 3, 2013 at 9:07
  • @Jack, was using recursion because I thought it was fun and (evidently) something I need excercising. Commented May 3, 2013 at 9:07
  • @FelixKling, thanks for mentioning that question. I guess that's the approach Jack was hinting at. Commented May 3, 2013 at 9:10

3 Answers 3

2

Instead of recursion I would choose for an iterative solution:

function setNestedPropertyValue(obj, fields, val)
{
  fields = fields.split('.');

  var cur = obj,
  last = fields.pop();

  fields.forEach(function(field) {
      cur[field] = {};
      cur = cur[field];
  });

  cur[last] = val;

  return obj;
}

setNestedPropertyValue({}, "grandpaprop.papaprop.babyprop", 1);
Sign up to request clarification or add additional context in comments.

Comments

2

EDIT

And here is another version thanks to the suggestions by Scott Sauyet:

function setPath(obj, [first, ...rest], val) {
    if (rest.length == 0) {
        return {...obj, [first]: val}
    }
    let nestedObj = obj[first] || {};
    return {...obj, [first]: setPath(nestedObj, rest, val)};
}

function setNestedPropertyValue(obj, field, val) {
    return setPath(obj, field.split('.'), val);
}

// example
let test_obj = {};
test_obj = setNestedPropertyValue(test_obj, "foo.bar.baz", 1);
test_obj = setNestedPropertyValue(test_obj, "foo.bar.baz1", 1);
// will output {"foo":{"bar":{"baz":1,"baz1":1}}}, while in the original version only "baz1" will be set
console.log(JSON.stringify(test_obj));

  • It's plain javascript
  • It only appends properties and will not override a top level object
  • setNestedPropertyValue() does not mutate the passed object (although keep in mind it only returns a shallow copy of the object, so some properties may be shared references between the original object and the new one)

I know this is old, but I needed exactly that kind of function and wasn't happy with the implementation, so here is my version:

function setNestedPropertyValue(obj, field, val) {
	if (field.indexOf(".") === -1) {
		obj[field] = val;
	} else {
		let fields = field.split(".");
		let topLevelField = fields.shift();
		let remainingFields = fields.join(".");
		if (obj[topLevelField] == null) {
			obj[topLevelField] = {};
		}
		setNestedPropertyValue(obj[topLevelField], remainingFields, val);
	}
}

// example
let test_obj = {};
setNestedPropertyValue(test_obj, "foo.bar.baz", 1);
setNestedPropertyValue(test_obj, "foo.bar.baz1", 1);
// will output {"foo":{"bar":{"baz":1,"baz1":1}}}, while in the original version only "baz1" will be set
console.log(JSON.stringify(test_obj)); 

  • It's plain javascript
  • It only appends properties and will not override a top level object
  • setNestedPropertyValue() does not return the object so it is clear that it mutates the passed object

4 Comments

For me, that third bullet is a serious negative. Mutating input data is the work of the devil! :-)
My own non-mutating version might be something like this functin taking an array for the field: const setPath = (obj, [f, ...fs], val) =>f == undefined ? obj : {...obj, [f]: fs .length ? setPath (obj [f] || {}, fs, val): val}, with a wrapper to use a dot-separated string: const setNestedPropertyValue = (obj, field, val) => setPath (obj, field.split('.'), val). It's not particularly sophisticated, and a library I maintain does quite a bit more, but it's enough for the cases here.
@ScottSauyet Haha, I totally agree about mutating input data :) I only meant it was a slight improvement on the original version which was a weird mix of the two. Thank you for your suggestions! I only played with a little bit of JS recently and I haven't come across the spread/rest operator. I've updated my answer based on your suggestions. I just prefer a more verbose approach so I can follow what's going on...
Rest/spread can DRY out a lot of code. It's definitely worth looking into.
0

As mentioned by Jack in the question, I was not returning my object in the last line in the else statement. By adding this, it is now working:

 obj[oneLevelLess] = setNestedPropertyValue( {}, remainingLevels, val);
   return obj; // Add this line
 }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.