how to convert this nested object into a flat object?

Sorry I don't know how to phrase the question title. Please help edit if possible.

I have an object like this:

{
    a: 'jack',
    b: {
        c: 'sparrow',
        d: {
           e: 'hahaha'
        }
    }
}

I want to make it look like:

{
    'a': 'jack',
    'b.c': 'sparrow',
    'b.d.e': 'hahaha'
}

// so that I can use it this way:
a['b.d.e']

jQuery is ok too. I know for the nested object, I can use a.b.d.e to get hahaha, but today I have to use it like a['b.d.e'] -_-!!! How can I achieve this? Thanks in advance :)


Solution 1:

You could use a recursive function to crawl the object and flatten it for you.

var test = {
    a: 'jack',
    b: {
        c: 'sparrow',
        d: {
            e: 'hahaha'
        }
    }
};

function traverseAndFlatten(currentNode, target, flattenedKey) {
    for (var key in currentNode) {
        if (currentNode.hasOwnProperty(key)) {
            var newKey;
            if (flattenedKey === undefined) {
                newKey = key;
            } else {
                newKey = flattenedKey + '.' + key;
            }

            var value = currentNode[key];
            if (typeof value === "object") {
                traverseAndFlatten(value, target, newKey);
            } else {
                target[newKey] = value;
            }
        }
    }
}

function flatten(obj) {
    var flattenedObject = {};
    traverseAndFlatten(obj, flattenedObject);
    return flattenedObject;
}

var flattened = JSON.stringify(flatten(test));
console.log(flattened);

Solution 2:

An alternative recursive implementation. I just felt like writing one implementation myself, even though the current ones are already really good.

The recursive function checks whether the key is of type 'object'.

  • If it's an object, we iterate by each object's key.
  • Else, we add it into our result object.
function flat(res, key, val, pre = '') {
  const prefix = [pre, key].filter(v => v).join('.');
  return typeof val === 'object'
    ? Object.keys(val).reduce((prev, curr) => flat(prev, curr, val[curr], prefix), res)
    : Object.assign(res, { [prefix]: val});
}
return Object.keys(input).reduce((prev, curr) => flat(prev, curr, input[curr]), {});

Flat NPM package

Or you can simply use flat npm package, which is a well known tested library.

var flatten = require('flat')
flatten(obj);

⬑ I would use this in serious code.

[Extra] Neater call to the function above

function flatObject(input) {
  function flat(res, key, val, pre = '') {
    const prefix = [pre, key].filter(v => v).join('.');
    return typeof val === 'object'
      ? Object.keys(val).reduce((prev, curr) => flat(prev, curr, val[curr], prefix), res)
      : Object.assign(res, { [prefix]: val});
  }

  return Object.keys(input).reduce((prev, curr) => flat(prev, curr, input[curr]), {});
}

const result = flatObject(input);

[Extra] Demo

http://codepen.io/zurfyx/pen/VpErja?editors=1010

function flatObject(input) {
  function flat(res, key, val, pre = '') {
    const prefix = [pre, key].filter(v => v).join('.');
    return typeof val === 'object'
      ? Object.keys(val).reduce((prev, curr) => flat(prev, curr, val[curr], prefix), res)
      : Object.assign(res, { [prefix]: val});
  }

  return Object.keys(input).reduce((prev, curr) => flat(prev, curr, input[curr]), {});
}

const result = flatObject({
    a: 'jack',
    b: {
        c: 'sparrow',
        d: {
           e: 'hahaha'
        }
    }
});

document.getElementById('code').innerHTML = JSON.stringify(result, null, 2);
<pre><code id="code"></code></pre>

Solution 3:

You could loop through the entries of the object. If the value is an object, recursively call the function. Use flatMap to get a flattened array of entries.

Then use Object.fromEntries() to get an object from the flattened array of entries

const input = {
  a: 'jack',
  b: {
    c: 'sparrow',
    d: {
      e: 'hahaha'
    }
  }
}

const getEntries = (o, prefix = '') => 
  Object.entries(o).flatMap(([k, v]) => 
    Object(v) === v  ? getEntries(v, `${prefix}${k}.`) : [ [`${prefix}${k}`, v] ]
  )

console.log(
  Object.fromEntries(getEntries(input))
)

Note: Object(v) === v returns true only for objects. typeof v === 'object' is true for v = null too.

Solution 4:

Option 1: export a flat object with just the Leaves. i.e object exported contains just paths with primitive value at the end ( see example ) .

//recursion: walk on each route until the primitive value.
//Did we found a primitive?
//Good, then join all keys in the current path and save it on the export object.
export function flatObject(obj) {
    const flatObject = {};
    const path = []; // current path

    function dig(obj) {
        if (obj !== Object(obj))
            /*is primitive, end of path*/
            return flatObject[path.join('.')] = obj; /*<- value*/ 
    
        //no? so this is an object with keys. go deeper on each key down
        for (let key in obj) {
            path.push(key);
            dig(obj[key]);
            path.pop();
        }
    }

    dig(obj);
    return flatObject;
}

Example

let  obj = {aaa:{bbb:{c:1,d:7}}, bb:{vv:2}}
console.log(flatObject(obj))
/*
{
  "aaa.bbb.c": 1,
  "aaa.bbb.d": 7,
  "bb.vv": 2
}
*/

Option 2: export a flat object with all intermidate paths. a little bit shorter and simpler (see example).

export function flatObject(obj) {
    const flatObject = {};
    const path = []; // current path

    function dig(obj) {
        for (let key in obj) {
            path.push(key);
            flatObject[path.join('.')] = obj[key];
            dig(obj[key])
            path.pop();
        }
    }

    dig(obj);
    return flatObject;
}

Example:

let  obj = {aaa:{bbb:{c:1,d:7}}, bb:{vv:2}}
console.log(flatObject(obj))
/*{
  "aaa": {
    "bbb": {
      "c": 1,
      "d": 7
    }
  },
  "aaa.bbb": {
    "c": 1,
    "d": 7
  },
  "aaa.bbb.c": 1,
  "aaa.bbb.d": 7,
  "bb": {
    "vv": 2
  },
  "bb.vv": 2
}
*/