31

I'm trying to build a function that would expand an object like :

{
    'ab.cd.e' : 'foo',
    'ab.cd.f' : 'bar',
    'ab.g' : 'foo2'
}

Into a nested object :

{ab: {cd: {e:'foo', f:'bar'}, g:'foo2'}}

Like this php function : Set::expand()

Without using eval of course.

2

10 Answers 10

48

I believe this is what you're after:

function deepen(obj) {
  const result = {};

  // For each object path (property key) in the object
  for (const objectPath in obj) {
    // Split path into component parts
    const parts = objectPath.split('.');

    // Create sub-objects along path as needed
    let target = result;
    while (parts.length > 1) {
      const part = parts.shift();
      target = target[part] = target[part] || {};
    }

    // Set value at end of path
    target[parts[0]] = obj[objectPath]
  }

  return result;
}

// For example ...
console.log(deepen({
  'ab.cd.e': 'foo',
  'ab.cd.f': 'bar',
  'ab.g': 'foo2'
}));

Sign up to request clarification or add additional context in comments.

4 Comments

I just put a more comprehensive solution down below: github.com/Gigzolo/dataobject-parser
This is risky if you have a same-name top level property. Wrap from t=oo; to t[key] = o[k] in if (k.indexOf('.') !== -1) ...
It also doesn't work if you have more than one top-level key.
@brandonscript I believe this works fine with multiple top-level properties. I tested with the pizza/this.other/this.thing.that example in your answer and get the same result as you.
9

If you're using Node.js (e.g. - if not cut and paste out of our module), try this package: https://www.npmjs.org/package/dataobject-parser

Built a module that does the forward/reverse operations:

https://github.com/Gigzolo/dataobject-parser

It's designed as a self managed object right now. Used by instantiating an instance of DataObjectParser.

var structured = DataObjectParser.transpose({
    'ab.cd.e' : 'foo',
    'ab.cd.f' : 'bar',
    'ab.g' : 'foo2'
});                                                                                                                                                                                                                                                                                                                                                                                                                                                   

structured.data() returns your nested object:

{ab: {cd: {e:'foo', f:'bar'}, g:'foo2'}}

So here's a working example in JSFiddle:

http://jsfiddle.net/H8Cqx/

Comments

5

Function name is terrible and the code was quickly made, but it should work. Note that this modifies the original object, I am not sure if you wanted to create a new object that is expanded version of the old one.

(function(){

    function parseDotNotation( str, val, obj ){
    var currentObj = obj,
        keys = str.split("."), i, l = keys.length - 1, key;

        for( i = 0; i < l; ++i ) {
        key = keys[i];
        currentObj[key] = currentObj[key] || {};
        currentObj = currentObj[key];
        }

    currentObj[keys[i]] = val;
    delete obj[str];
    }

    Object.expand = function( obj ) {

        for( var key in obj ) {
        parseDotNotation( key, obj[key], obj );
        }
    return obj;
    };

})();



var expanded = Object.expand({
    'ab.cd.e' : 'foo',
        'ab.cd.f' : 'bar',
    'ab.g' : 'foo2'
});



JSON.stringify( expanded );  


//"{"ab":{"cd":{"e":"foo","f":"bar"},"g":"foo2"}}"

1 Comment

This is great, but only works if there is a single top-level key. If you had {"foo":"bar", "foo.baz":"bar", "baz":"bar"} it'll drop the "baz" key.
5

You could split the key string as path and reduce it for assigning the value by using a default object for unvisited levels.

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

    keys.reduce((o, k) => o[k] = o[k] || {}, object)[last] = value;
    return object;
}

var source = { 'ab.cd.e': 'foo', 'ab.cd.f': 'bar', 'ab.g': 'foo2' },
    target = Object
        .entries(source)
        .reduce((o, [k, v]) => setValue(o, k, v), {});

console.log(target);

8 Comments

what if we have a key repeated twice? looks like it is keeping the last value only...
@user3174311, right. what would you like to get instead?
in case a key is repeated I would like to get an array or object. thank you.
@user3174311, with this special case, you could check if the property has a value and change the type to array with the first value. but this needs to check if a value is already given, too. maybe you ask a new question with this problem and add what you have tried, as well.
I asked it here: stackoverflow.com/questions/58482952/… I am going to add some come I tried. Thank you.
|
3

Derived from Esailija's answer, with fixes to support multiple top-level keys.

(function () {
    function parseDotNotation(str, val, obj) {
        var currentObj = obj,
            keys = str.split("."),
            i, l = Math.max(1, keys.length - 1),
            key;

        for (i = 0; i < l; ++i) {
            key = keys[i];
            currentObj[key] = currentObj[key] || {};
            currentObj = currentObj[key];
        }

        currentObj[keys[i]] = val;
        delete obj[str];
    }

    Object.expand = function (obj) {
        for (var key in obj) {
            if (key.indexOf(".") !== -1)
            {
                parseDotNotation(key, obj[key], obj);
            }            
        }
        return obj;
    };

})();

var obj = {
    "pizza": "that",
    "this.other": "that",
    "alphabets": [1, 2, 3, 4],
    "this.thing.that": "this"
}

Outputs:

{
    "pizza": "that",
    "alphabets": [
        1,
        2,
        3,
        4
    ],
    "this": {
        "other": "that",
        "thing": {
            "that": "this"
        }
    }
}

Fiddle

Comments

2

ES6 one-liner:

const data = {
  'ab.cd.e' : 'foo',
  'ab.cd.f' : 'bar',
  'ab.g' : 'foo2'
}

const result = Object.entries(data).reduce((a,[p,v])=>
  (p.split('.').reduce((b,k,i,r)=>(b[k]??=(i===r.length-1?v:{})),a),a),{})

console.log(result)

1 Comment

nice one. but hard do read :/
1

You need to convert each string key into object. Using following function you can get desire result.

 function convertIntoJSON(obj) {

                var o = {}, j, d;
                for (var m in obj) {
                    d = m.split(".");
                var startOfObj = o;
                for (j = 0; j < d.length  ; j += 1) {

                    if (j == d.length - 1) {
                        startOfObj[d[j]] = obj[m];
                    }
                    else {
                        startOfObj[d[j]] = startOfObj[d[j]] || {};
                        startOfObj = startOfObj[d[j]];
                    }
                }
            }
            return o;
        }

Now call this function

 var aa = {
                'ab.cd.e': 'foo',
                'ab.cd.f': 'bar',
                    'ab.g': 'foo2'
                };
   var desiredObj =  convertIntoJSON(aa);

Comments

1

Something that works, but is probably not the most efficient way to do so (also relies on ECMA 5 Object.keys() method, but that can be easily replaced.

var input = {
    'ab.cd.e': 'foo',
    'ab.cd.f': 'bar',
    'ab.g': 'foo2'
};

function createObjects(parent, chainArray, value) {
    if (chainArray.length == 1) {
        parent[chainArray[0]] = value;
        return parent;
    }
    else {
        parent[chainArray[0]] = parent[chainArray[0]] || {};
        return createObjects(parent[chainArray[0]], chainArray.slice(1, chainArray.length), value);
    }
}

var keys = Object.keys(input);
var result = {};

for(var i = 0, l = keys.length; i < l; i++)
{
    createObjects(result, keys[i].split('.'), input[keys[i]]);
}

JSFiddle is here.

Comments

1

Here is how I do this in one of my applications:

const obj = {
  "start.headline": "1 headline",
  "start.subHeadline": "subHeadline",
  "start.accordion.headline": "2 headline",
  "start.accordion.sections.0.content": "content 0",
  "start.accordion.sections.0.iconName": "icon 0",
  "start.accordion.sections.1.headline": "headline 1",
  "start.accordion.sections.1.content": "content 1",
  "start.accordion.sections.1.iconName": "icon 1",
  "start.accordion.sections.2.headline": "headline 2",
  "start.accordion.sections.2.content": "content 2",
  "start.accordion.sections.2.iconName": "icon 2",
  "end.field": "add headline",
  "end.button": "add button",
  "end.msgs.success": "success msg",
  "end.msgs.error": "error msg",
};

const res = Object.keys(obj).reduce((res, key) => {
  const path = key.split('.');
  const lastIndex = path.length - 1;
  path.reduce(
    (acc, k, i, a) => acc[k] = lastIndex === i ?
    obj[key] :
    acc[k] || (/\d/.test(a[i+1]) ? [] : {}),
    res
  );
  return res;
}, {});

console.log(res);

Comments

-1

This is the answer as provided by @broofa, but converted to TypeScript.

type NestedObject = { [key: string]: any };

function objectify(obj: NestedObject): NestedObject {
  const result: NestedObject = {};
  for (const key in obj) {
    let target: NestedObject = result;
    const parts = key.split(".");
    for (let j = 0; j < parts.length - 1; j++) {
      const part = parts[j];
      target = target[part] = target[part] || {};
    }
    target[parts[parts.length - 1]] = obj[key];
  }
  return result;
}

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.