29

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.

Kara
  • 6,115
  • 16
  • 50
  • 57
Olivier
  • 3,431
  • 5
  • 39
  • 56
  • 1
    related: [Fastest way to flatten / un-flatten nested JSON objects](http://stackoverflow.com/q/19098797/1048572) – Bergi Dec 09 '15 at 22:09
  • If it should also work for nested objects, see here: https://silvantroxler.ch/2018/object-string-property-nesting/ – str Nov 14 '18 at 14:00

10 Answers10

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'
}));
broofa
  • 37,461
  • 11
  • 73
  • 73
  • 3
    I just put a more comprehensive solution down below: https://github.com/Gigzolo/dataobject-parser – Henry Tseng Mar 06 '14 at 19:37
  • 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)` ... – brandonscript Apr 14 '14 at 19:14
  • It also doesn't work if you have more than one top-level key. – brandonscript Apr 14 '14 at 19:15
  • @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. – broofa Jul 27 '20 at 12:55
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/

Henry Tseng
  • 3,263
  • 1
  • 19
  • 20
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);
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • what if we have a key repeated twice? looks like it is keeping the last value only... – user3174311 Oct 21 '19 at 09:26
  • @user3174311, right. what would you like to get instead? – Nina Scholz Oct 21 '19 at 09:33
  • in case a key is repeated I would like to get an array or object. thank you. – user3174311 Oct 21 '19 at 09:38
  • @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. – Nina Scholz Oct 21 '19 at 09:57
  • I asked it here: https://stackoverflow.com/questions/58482952/how-to-split-a-string-into-an-associative-array-in-js I am going to add some come I tried. Thank you. – user3174311 Oct 21 '19 at 10:01
  • sorry, i hadn't read the question in depth. what you want is a special case. – Nina Scholz Oct 21 '19 at 10:03
  • how can I change this to only returns array? even when we have a single element? – user3174311 Oct 21 '19 at 11:15
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/201200/discussion-between-nina-scholz-and-user3174311). – Nina Scholz Oct 21 '19 at 11:16
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"}}"
Esailija
  • 138,174
  • 23
  • 272
  • 326
  • 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. – brandonscript Apr 14 '14 at 19:34
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

Community
  • 1
  • 1
brandonscript
  • 68,675
  • 32
  • 163
  • 220
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);
Yordan Nikolov
  • 2,598
  • 13
  • 16
1

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)
Andrew Parks
  • 6,358
  • 2
  • 12
  • 27
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);
Anoop
  • 23,044
  • 10
  • 62
  • 76
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.

ZenMaster
  • 12,363
  • 5
  • 36
  • 59
-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;
}
SeverityOne
  • 2,476
  • 12
  • 25