212

I threw some code together to flatten and un-flatten complex/nested JavaScript objects. It works, but it's a bit slow (triggers the 'long script' warning).

For the flattened names I want "." as the delimiter and [INDEX] for arrays.

Examples:

un-flattened | flattened
---------------------------
{foo:{bar:false}} => {"foo.bar":false}
{a:[{b:["c","d"]}]} => {"a[0].b[0]":"c","a[0].b[1]":"d"}
[1,[2,[3,4],5],6] => {"[0]":1,"[1].[0]":2,"[1].[1].[0]":3,"[1].[1].[1]":4,"[1].[2]":5,"[2]":6}

I created a benchmark that ~simulates my use case http://jsfiddle.net/WSzec/

  • Get a nested object
  • Flatten it
  • Look through it and possibly modify it while flattened
  • Unflatten it back to it's original nested format to be shipped away

I would like faster code: For clarification, code that completes the JSFiddle benchmark (http://jsfiddle.net/WSzec/) significantly faster (~20%+ would be nice) in IE 9+, FF 24+, and Chrome 29+.

Here's the relevant JavaScript code: Current Fastest: http://jsfiddle.net/WSzec/6/

var unflatten = function(data) {
    "use strict";
    if (Object(data) !== data || Array.isArray(data))
        return data;
    var result = {}, cur, prop, idx, last, temp;
    for(var p in data) {
        cur = result, prop = "", last = 0;
        do {
            idx = p.indexOf(".", last);
            temp = p.substring(last, idx !== -1 ? idx : undefined);
            cur = cur[prop] || (cur[prop] = (!isNaN(parseInt(temp)) ? [] : {}));
            prop = temp;
            last = idx + 1;
        } while(idx >= 0);
        cur[prop] = data[p];
    }
    return result[""];
}
var flatten = function(data) {
    var result = {};
    function recurse (cur, prop) {
        if (Object(cur) !== cur) {
            result[prop] = cur;
        } else if (Array.isArray(cur)) {
             for(var i=0, l=cur.length; i<l; i++)
                 recurse(cur[i], prop ? prop+"."+i : ""+i);
            if (l == 0)
                result[prop] = [];
        } else {
            var isEmpty = true;
            for (var p in cur) {
                isEmpty = false;
                recurse(cur[p], prop ? prop+"."+p : p);
            }
            if (isEmpty)
                result[prop] = {};
        }
    }
    recurse(data, "");
    return result;
}

EDIT 1 Modified the above to @Bergi 's implementation which is currently the fastest. As an aside, using ".indexOf" instead of "regex.exec" is around 20% faster in FF but 20% slower in Chrome; so I'll stick with the regex since it's simpler (here's my attempt at using indexOf to replace the regex http://jsfiddle.net/WSzec/2/).

EDIT 2 Building on @Bergi 's idea I managed to created a faster non-regex version (3x faster in FF and ~10% faster in Chrome). http://jsfiddle.net/WSzec/6/ In the this (the current) implementation the rules for key names are simply, keys cannot start with an integer or contain a period.

Example:

  • {"foo":{"bar":[0]}} => {"foo.bar.0":0}

EDIT 3 Adding @AaditMShah 's inline path parsing approach (rather than String.split) helped to improve the unflatten performance. I'm very happy with the overall performance improvement reached.

The latest jsfiddle and jsperf:

http://jsfiddle.net/WSzec/14/

http://jsperf.com/flatten-un-flatten/4

trincot
  • 317,000
  • 35
  • 244
  • 286
Louis Ricci
  • 20,804
  • 5
  • 48
  • 62
  • 9
    [There is no such thing as a "JSON object"](http://benalman.com/news/2010/03/theres-no-such-thing-as-a-json/). The question seems to be about JS objects. – Felix Kling Sep 30 '13 at 16:08
  • 1
    This question seems to be more appropriate for the Code Review StackExchange site: http://codereview.stackexchange.com/ – Aadit M Shah Sep 30 '13 at 16:17
  • 6
    @FelixKling - By JSON object I meant JS objects that only contain primitive JavaScript types. You could, for instance, put an function in a JS object, but it would not be serialized into JSON -- i.e. JSON.stringify({fn:function(){alert('a');}}); -- – Louis Ricci Sep 30 '13 at 16:19
  • @AaditMShah - I'm looking for optimization of a current algorithm OR a faster means of completing the same task. Perhaps I should tag this with algorithm as well... ? – Louis Ricci Sep 30 '13 at 16:21
  • @Andy - I want to be able to iterate through the key->values of, and possibly edit the flattened object (add more keys, update values, remove keys). – Louis Ricci Sep 30 '13 at 16:24
  • 2
    `[1].[1].[0]` looks wrong to me. Are you sure this is the desired result? – Bergi Sep 30 '13 at 16:25
  • @bergi - Yes. I originally created the code for a specific nested object type, but later thought it would be prudent to make it also work with nested arrays. While un-flattening I found using delimiter between each indexer (even for nested arrays) was cleaner. If there's a significantly faster way to do it as "[1][1][0]" then I'd be open it. – Louis Ricci Sep 30 '13 at 16:33
  • Are your keys always supposed to be legal? For example given the object `{ foo: { bar: { "[0]": true } } }` should the flattened version be `{ "foo.bar['[0]']": true }` or `{ "foo.bar.[0]": true }`? It seems like there's scope for ambiguities in your implementation. – Aadit M Shah Oct 03 '13 at 01:50
  • Would you elaborate on *Look through it and possibly modify it while flattened* step, perhaps a sample action? Perhaps you trying to use vanilla `.map` or similar on the flattened object? – Oleg Oct 03 '13 at 06:28
  • @AaditMShah - As long as the key format is consistent and the rule / constraint for key names is simple (e.x. no ".", "[" or "]" in key names) then my project won't run into any ambiguities. "a.b[0]" vs "a.b.[0]" is just an implementation preference I don't mind what the resulting flat key looks like as long as the constraint is simple and it runs quickly. – Louis Ricci Oct 03 '13 at 21:56
  • @o.v. - On the client side: a large nested form -> serialize it (nested JSON) -> flatten it (data) and a key/val map of business rules and/or validation rules (rules). The keys in rules would be "foo.bar[]", while the keys in the flat data would be "foo.bar[0]" and "foo.bar[1]". To run all of the rules that apply to data you could loop through the keys in data, clean the indices (integers) out of the key name, and look it up in rules. On the server side (node js or google apps script): would be the same as the client side except you'd merge in admin / meta information into data before rule chk – Louis Ricci Oct 03 '13 at 22:08
  • @LastCoder: I must be having a slow day; when you say "nested form", do you refer to [complex serialization](http://stackoverflow.com/a/1186309/1081234) or actual `form` elements nested, which [is invalid in xhtml](http://stackoverflow.com/q/597596/1081234) or [html 5](http://stackoverflow.com/a/379622/1081234)? – Oleg Oct 03 '13 at 22:45
  • @o.v. - That was an ambiguous choice of words on my part. When I said 'nested form' I just meant a single FORM element that contains data that can represent data with possible one-to-many relationships. Example SQL Tables : Person(Key PK, Name), Emails(PersonID FK, Email). So the client side representation would be a form that would serialize to {Key: "", Name: "", Emails: [{Email:""},{Email:""}]}. In an abstract sense the single form represents a 'Person' has ~nested 'Emails' in it. So, a single form that represents complex / nested data. – Louis Ricci Oct 04 '13 at 01:30
  • Is there a standard jsperf to prove performance? – SheetJS Oct 06 '13 at 03:31
  • @Nirk There are three: 1) http://jsperf.com/flatten-a-json-object/2 2) http://jsperf.com/unflatten-a-json-object 3) http://jsperf.com/flatten-and-unflatten-a-json-object Read my answer for more details: http://stackoverflow.com/a/19204980/783743 – Aadit M Shah Oct 06 '13 at 06:50
  • @Nirk - jsperf.com/flatten-un-flatten Here's the jsperf I put together, using a larger piece of test data. – Louis Ricci Oct 06 '13 at 15:40
  • @LastCoder, if you have a second, jump over to this question and tell us why anyone would ever need to flatten JSON. Thanks, man. http://stackoverflow.com/questions/24833379/why-do-we-need-to-flatten-json-objects – Jonathan M Jul 18 '14 at 20:55
  • 2
    There's a bug unfortunately: Date objects are converted to an empty JSON. – giacecco Mar 28 '16 at 15:09
  • @giacecco - If you want to use Date/Times with JSON it's best to keep them in ISO format *Date.prototype.toISOString* and only convert to the actual Date obect when calculations or display logic is necessary. Modern browsers (es6 compatible) will convert Date objects to ISO string upon JSON.stringify, but on JSON.parse the ISO strings do notget converted back into Date objects automatically (they are indistinguishable from other strings). This flatten / unflatten code is a bit out of date (excuse the pun) now. – Louis Ricci Mar 30 '16 at 11:21
  • 3
    Hi, why don't you create an answer instead of putting the answer in the question ? With all those edits it is very cluttered... – TOPKAT Apr 01 '19 at 12:59

18 Answers18

262

Here's my much shorter implementation:

Object.unflatten = function(data) {
    "use strict";
    if (Object(data) !== data || Array.isArray(data))
        return data;
    var regex = /\.?([^.\[\]]+)|\[(\d+)\]/g,
        resultholder = {};
    for (var p in data) {
        var cur = resultholder,
            prop = "",
            m;
        while (m = regex.exec(p)) {
            cur = cur[prop] || (cur[prop] = (m[2] ? [] : {}));
            prop = m[2] || m[1];
        }
        cur[prop] = data[p];
    }
    return resultholder[""] || resultholder;
};

flatten hasn't changed much (and I'm not sure whether you really need those isEmpty cases):

Object.flatten = function(data) {
    var result = {};
    function recurse (cur, prop) {
        if (Object(cur) !== cur) {
            result[prop] = cur;
        } else if (Array.isArray(cur)) {
             for(var i=0, l=cur.length; i<l; i++)
                 recurse(cur[i], prop + "[" + i + "]");
            if (l == 0)
                result[prop] = [];
        } else {
            var isEmpty = true;
            for (var p in cur) {
                isEmpty = false;
                recurse(cur[p], prop ? prop+"."+p : p);
            }
            if (isEmpty && prop)
                result[prop] = {};
        }
    }
    recurse(data, "");
    return result;
}

Together, they run your benchmark in about the half of the time (Opera 12.16: ~900ms instead of ~ 1900ms, Chrome 29: ~800ms instead of ~1600ms).

Note: This and most other solutions answered here focus on speed and are susceptible to prototype pollution and shold not be used on untrusted objects.

Alex Brasetvik
  • 11,218
  • 2
  • 35
  • 36
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 1
    This is great! The regex runs remarkably well (especially in Chrome), I tried replacing it with indexOf logic, but was only able to realize a speed-up in FF. I'll be adding a bounty to this question to see if another clever improvement can be stirred up, but so far this is more than what I was hoping for. – Louis Ricci Oct 01 '13 at 20:50
  • 1
    I managed to grind more speed out of your implementation by replacing the regex.exec() with string.split() and simplifying the key format. I'll give it a few days before I award you the pts, but I think the 'wall of meaningful optimization' has been reached. – Louis Ricci Oct 05 '13 at 21:52
  • JSON.flatten({}); // { '': {} } -- you could add a line after var result = {}; -- if (result === data) return data; – Ivan Nov 29 '13 at 21:20
  • @Ivan: Ah, thanks for that edge case, though semantically it actually would be required to have an extra representation for empty objects. But no, `result === data` won't work, they're never identical. – Bergi Nov 30 '13 at 12:29
  • @Bergi Yeah you're right. Object.keys(data).length === 0 works though – Ivan Dec 05 '13 at 01:43
  • Condition `Object(cur) !== cur` is object instance *detector*, but the main problem is that this doesn't work for `Date` instances as `Object(dateInstance) === dateInstance`. A strict object detection is by using `toString` which may impede performance a tiny bit but it would work correctly for all types. Your conditional `if` statements should of course change. `Object.prototype.toString.call(curr)` returns `[object Object]` for objects only and `[object Array]` for arrays. Other interesting types are as follows: `[object Null]`, `[object Undefined]`, `[object Date]`, `[object Symbol]` (ES6) – Robert Koritnik Dec 12 '14 at 12:11
  • @RobertKoritnik: I'd rather call it a "primitive value detector". The code does not care about regexes, dates, functions or any other custom objects (except arrays) as those are not valid in **JSON** anyway. If you need a generic version, you surely can adapt it. – Bergi Dec 12 '14 at 12:21
  • Nice, but one remark: these methods have little to do with JSON, as they do not input or output strings, but objects, and so these functions should not be defined on the JSON object. – trincot Mar 18 '17 at 00:06
  • @trincot True, I just carelessly copied that over from the OP. Would the `Object` "namespace" fit better? – Bergi Mar 18 '17 at 15:29
  • Ah, Bergi, I did not realise until now it was your answer. But from 2013, so forgiveable :-) Probably would fit better on `Object`, but you know best! – trincot Mar 18 '17 at 15:33
  • The code is bad, (modifying Object or JSON), functions are not the fastest possible,and using Map is 40% faster http://jsfiddle.net/crl/WSzec/153/ and author reject edit when you try to improve it – caub Mar 22 '17 at 19:18
  • @caub Thanks for the ideas, but this should not be an edit. Please post your own answer with the faster solution (and notice that `Map`s weren't a thing in 2013 :-D) – Bergi Mar 22 '17 at 19:24
  • @Bergi ok, I noticed the 'bad' `JSON.flatten` was from OP, you're welcome, I can't do better than your unflatten http://jsfiddle.net/crl/WSzec/154/, yes, true, Map are post 2013 – caub Mar 22 '17 at 20:18
  • This implementation was susceptible to a prototype pollution problem, which if chained with other things can be a big deal: https://github.com/lodash/lodash/pull/4518 – Alex Brasetvik Feb 19 '20 at 13:00
  • @AlexBrasetvik I've rolled back that edit, one should never call `hasOwnProperty` frrom the object itself. If at all, [`Object.prototype.hasOwnProperty.call(…)` should be used](https://stackoverflow.com/a/45014721/1048572). But really, this code was not meant to be used with untrusted objects - if you require that, use `Object.create(null)` instead of `{}`. – Bergi Feb 19 '20 at 18:25
  • I added a note I hope is acceptable. Devs landing on this answer are not necessarily after the fastest implementation. Here's an example of this exact code being used in a way that was unsafe: https://github.com/elastic/kibana/commit/6e63c2c944d8aaa8d2a02904d6f7acf482a0dfd2 Combined with other weaknesses that prototype pollution enabled a remote code execution vulnerability: https://research.securitum.com/prototype-pollution-rce-kibana-cve-2019-7609/ I understand that this particular post might be about speed and that security is sacrificed, but I hope the note is acceptable at least :) – Alex Brasetvik Feb 20 '20 at 13:28
  • @AlexBrasetvik Thanks. Btw, as a fix for kibana I'd recommend `cur = _.has(cur, prop) ? cur[prop] : (cur[prop] = …)`, much better than abusing `&&`+`||` for a conditional. – Bergi Feb 20 '20 at 13:58
36

I wrote two functions to flatten and unflatten a JSON object.


Flatten a JSON object:

var flatten = (function (isArray, wrapped) {
    return function (table) {
        return reduce("", {}, table);
    };

    function reduce(path, accumulator, table) {
        if (isArray(table)) {
            var length = table.length;

            if (length) {
                var index = 0;

                while (index < length) {
                    var property = path + "[" + index + "]", item = table[index++];
                    if (wrapped(item) !== item) accumulator[property] = item;
                    else reduce(property, accumulator, item);
                }
            } else accumulator[path] = table;
        } else {
            var empty = true;

            if (path) {
                for (var property in table) {
                    var item = table[property], property = path + "." + property, empty = false;
                    if (wrapped(item) !== item) accumulator[property] = item;
                    else reduce(property, accumulator, item);
                }
            } else {
                for (var property in table) {
                    var item = table[property], empty = false;
                    if (wrapped(item) !== item) accumulator[property] = item;
                    else reduce(property, accumulator, item);
                }
            }

            if (empty) accumulator[path] = table;
        }

        return accumulator;
    }
}(Array.isArray, Object));

Performance:

  1. It's faster than the current solution in Opera. The current solution is 26% slower in Opera.
  2. It's faster than the current solution in Firefox. The current solution is 9% slower in Firefox.
  3. It's faster than the current solution in Chrome. The current solution is 29% slower in Chrome.

Unflatten a JSON object:

function unflatten(table) {
    var result = {};

    for (var path in table) {
        var cursor = result, length = path.length, property = "", index = 0;

        while (index < length) {
            var char = path.charAt(index);

            if (char === "[") {
                var start = index + 1,
                    end = path.indexOf("]", start),
                    cursor = cursor[property] = cursor[property] || [],
                    property = path.slice(start, end),
                    index = end + 1;
            } else {
                var cursor = cursor[property] = cursor[property] || {},
                    start = char === "." ? index + 1 : index,
                    bracket = path.indexOf("[", start),
                    dot = path.indexOf(".", start);

                if (bracket < 0 && dot < 0) var end = index = length;
                else if (bracket < 0) var end = index = dot;
                else if (dot < 0) var end = index = bracket;
                else var end = index = bracket < dot ? bracket : dot;

                var property = path.slice(start, end);
            }
        }

        cursor[property] = table[path];
    }

    return result[""];
}

Performance:

  1. It's faster than the current solution in Opera. The current solution is 5% slower in Opera.
  2. It's slower than the current solution in Firefox. My solution is 26% slower in Firefox.
  3. It's slower than the current solution in Chrome. My solution is 6% slower in Chrome.

Flatten and unflatten a JSON object:

Overall my solution performs either equally well or even better than the current solution.

Performance:

  1. It's faster than the current solution in Opera. The current solution is 21% slower in Opera.
  2. It's as fast as the current solution in Firefox.
  3. It's faster than the current solution in Firefox. The current solution is 20% slower in Chrome.

Output format:

A flattened object uses the dot notation for object properties and the bracket notation for array indices:

  1. {foo:{bar:false}} => {"foo.bar":false}
  2. {a:[{b:["c","d"]}]} => {"a[0].b[0]":"c","a[0].b[1]":"d"}
  3. [1,[2,[3,4],5],6] => {"[0]":1,"[1][0]":2,"[1][1][0]":3,"[1][1][1]":4,"[1][2]":5,"[2]":6}

In my opinion this format is better than only using the dot notation:

  1. {foo:{bar:false}} => {"foo.bar":false}
  2. {a:[{b:["c","d"]}]} => {"a.0.b.0":"c","a.0.b.1":"d"}
  3. [1,[2,[3,4],5],6] => {"0":1,"1.0":2,"1.1.0":3,"1.1.1":4,"1.2":5,"2":6}

Advantages:

  1. Flattening an object is faster than the current solution.
  2. Flattening and unflattening an object is as fast as or faster than the current solution.
  3. Flattened objects use both the dot notation and the bracket notation for readability.

Disadvantages:

  1. Unflattening an object is slower than the current solution in most (but not all) cases.

The current JSFiddle demo gave the following values as output:

Nested : 132175 : 63
Flattened : 132175 : 564
Nested : 132175 : 54
Flattened : 132175 : 508

My updated JSFiddle demo gave the following values as output:

Nested : 132175 : 59
Flattened : 132175 : 514
Nested : 132175 : 60
Flattened : 132175 : 451

I'm not really sure what that means, so I'll stick with the jsPerf results. After all jsPerf is a performance benchmarking utility. JSFiddle is not.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • Very cool. I really like the style for flatten, using anonymous functions to get Array.isArray and Object into a closer scope. I think the the test object your using for the JSPerf test is too simple though. I created the object "fillObj({},4)" in my jsfiddle benchmark to emulate a real case of a large complex nested piece of data. – Louis Ricci Oct 06 '13 at 15:04
  • Show me the code for your object and I'll incorporate it into the benchmark. – Aadit M Shah Oct 06 '13 at 15:08
  • 2
    @LastCoder Hmmm, your current implementation seems to be faster than mine in most browsers (notably Firefox). Interestingly my implementation is faster in Opera and it's not so bad in Chrome either. I don't think having such a large data set is an ideal factor to determine the speed of the algorithm because: 1) large data sets need a large amount of memory, page swapping, etc.; and that's not something you can control in JS (i.e. you're at the mercy of the browser) 2) if you want to do CPU intensive work then JS is not the best language. Consider using C instead. There are JSON libraries for C – Aadit M Shah Oct 06 '13 at 16:51
  • 1
    that's a good point and brings up the difference between synthetic vs real world benchmarking. I'm happy with the performance of the current optimized JS, so no need to use C. – Louis Ricci Oct 07 '13 at 13:54
  • This implementation also has a prototype pollution bug, e.g. `unflatten({"foo.__proto__.bar": 42})` – Alex Brasetvik Feb 19 '20 at 14:06
21

ES6 version:

const flatten = (obj, path = '') => {        
    if (!(obj instanceof Object)) return {[path.replace(/\.$/g, '')]:obj};

    return Object.keys(obj).reduce((output, key) => {
        return obj instanceof Array ? 
             {...output, ...flatten(obj[key], path +  '[' + key + '].')}:
             {...output, ...flatten(obj[key], path + key + '.')};
    }, {});
}

Example:

console.log(flatten({a:[{b:["c","d"]}]}));
console.log(flatten([1,[2,[3,4],5],6]));
Guy
  • 12,488
  • 16
  • 79
  • 119
14

3 ½ Years later...

For my own project I wanted to flatten JSON objects in mongoDB dot notation and came up with a simple solution:

/**
 * Recursively flattens a JSON object using dot notation.
 *
 * NOTE: input must be an object as described by JSON spec. Arbitrary
 * JS objects (e.g. {a: () => 42}) may result in unexpected output.
 * MOREOVER, it removes keys with empty objects/arrays as value (see
 * examples bellow).
 *
 * @example
 * // returns {a:1, 'b.0.c': 2, 'b.0.d.e': 3, 'b.1': 4}
 * flatten({a: 1, b: [{c: 2, d: {e: 3}}, 4]})
 * // returns {a:1, 'b.0.c': 2, 'b.0.d.e.0': true, 'b.0.d.e.1': false, 'b.0.d.e.2.f': 1}
 * flatten({a: 1, b: [{c: 2, d: {e: [true, false, {f: 1}]}}]})
 * // return {a: 1}
 * flatten({a: 1, b: [], c: {}})
 *
 * @param obj item to be flattened
 * @param {Array.string} [prefix=[]] chain of prefix joined with a dot and prepended to key
 * @param {Object} [current={}] result of flatten during the recursion
 *
 * @see https://docs.mongodb.com/manual/core/document/#dot-notation
 */
function flatten (obj, prefix, current) {
  prefix = prefix || []
  current = current || {}

  // Remember kids, null is also an object!
  if (typeof (obj) === 'object' && obj !== null) {
    Object.keys(obj).forEach(key => {
      this.flatten(obj[key], prefix.concat(key), current)
    })
  } else {
    current[prefix.join('.')] = obj
  }

  return current
}

Features and/or caveats

  • It only accepts JSON objects. So if you pass something like {a: () => {}} you might not get what you wanted!
  • It removes empty arrays and objects. So this {a: {}, b: []} is flattened to {}.
Yan Foto
  • 10,850
  • 6
  • 57
  • 88
10

Use this library:

npm install flat

Usage (from https://www.npmjs.com/package/flat):

Flatten:

    var flatten = require('flat')


    flatten({
        key1: {
            keyA: 'valueI'
        },
        key2: {
            keyB: 'valueII'
        },
        key3: { a: { b: { c: 2 } } }
    })

    // {
    //   'key1.keyA': 'valueI',
    //   'key2.keyB': 'valueII',
    //   'key3.a.b.c': 2
    // }

Un-flatten:

var unflatten = require('flat').unflatten

unflatten({
    'three.levels.deep': 42,
    'three.levels': {
        nested: true
    }
})

// {
//     three: {
//         levels: {
//             deep: 42,
//             nested: true
//         }
//     }
// }
o.z
  • 1,086
  • 14
  • 22
6

Here's another approach that runs slower (about 1000ms) than the above answer, but has an interesting idea :-)

Instead of iterating through each property chain, it just picks the last property and uses a look-up-table for the rest to store the intermediate results. This look-up-table will be iterated until there are no property chains left and all values reside on uncocatenated properties.

JSON.unflatten = function(data) {
    "use strict";
    if (Object(data) !== data || Array.isArray(data))
        return data;
    var regex = /\.?([^.\[\]]+)$|\[(\d+)\]$/,
        props = Object.keys(data),
        result, p;
    while(p = props.shift()) {
        var m = regex.exec(p),
            target;
        if (m.index) {
            var rest = p.slice(0, m.index);
            if (!(rest in data)) {
                data[rest] = m[2] ? [] : {};
                props.push(rest);
            }
            target = data[rest];
        } else {
            target = result || (result = (m[2] ? [] : {}));
        }
        target[m[2] || m[1]] = data[p];
    }
    return result;
};

It currently uses the data input parameter for the table, and puts lots of properties on it - a non-destructive version should be possible as well. Maybe a clever lastIndexOf usage performs better than the regex (depends on the regex engine).

See it in action here.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I didn't downvote your answer. However I would like to point out that your function doesn't `unflatten` the flattened object correctly. For example consider the array `[1,[2,[3,4],5],6]`. Your `flatten` function flattens this object to `{"[0]":1,"[1][0]":2,"[1][1][0]":3,"[1][1][1]":4,"[1][2]":5,"[2]":6}`. Your `unflatten` function however incorrectly unflattens the flattened object to `[1,[null,[3,4]],6]`. The reason this happens is because of the statement `delete data[p]` which prematurely deletes the intermediate value `[2,null,5]` before `[3,4]` is added to it. Use a stack to solve it. :-) – Aadit M Shah Oct 06 '13 at 16:30
  • 1
    Ah, I see, undefined enumeration order… Gonna fix it with a queue of properties, please put your stack solution in an own answer. Thanks for the hint! – Bergi Oct 06 '13 at 18:22
6

You can use https://github.com/hughsk/flat

Take a nested Javascript object and flatten it, or unflatten an object with delimited keys.

Example from the doc

var flatten = require('flat')

flatten({
    key1: {
        keyA: 'valueI'
    },
    key2: {
        keyB: 'valueII'
    },
    key3: { a: { b: { c: 2 } } }
})

// {
//   'key1.keyA': 'valueI',
//   'key2.keyB': 'valueII',
//   'key3.a.b.c': 2
// }


var unflatten = require('flat').unflatten

unflatten({
    'three.levels.deep': 42,
    'three.levels': {
        nested: true
    }
})

// {
//     three: {
//         levels: {
//             deep: 42,
//             nested: true
//         }
//     }
// }
Tom Esterez
  • 21,567
  • 8
  • 39
  • 44
3

This code recursively flattens out JSON objects.

I included my timing mechanism in the code and it gives me 1ms but I'm not sure if that's the most accurate one.

            var new_json = [{
              "name": "fatima",
              "age": 25,
              "neighbour": {
                "name": "taqi",
                "location": "end of the street",
                "property": {
                  "built in": 1990,
                  "owned": false,
                  "years on market": [1990, 1998, 2002, 2013],
                  "year short listed": [], //means never
                }
              },
              "town": "Mountain View",
              "state": "CA"
            },
            {
              "name": "qianru",
              "age": 20,
              "neighbour": {
                "name": "joe",
                "location": "opposite to the park",
                "property": {
                  "built in": 2011,
                  "owned": true,
                  "years on market": [1996, 2011],
                  "year short listed": [], //means never
                }
              },
              "town": "Pittsburgh",
              "state": "PA"
            }]

            function flatten(json, flattened, str_key) {
                for (var key in json) {
                  if (json.hasOwnProperty(key)) {
                    if (json[key] instanceof Object && json[key] != "") {
                      flatten(json[key], flattened, str_key + "." + key);
                    } else {
                      flattened[str_key + "." + key] = json[key];
                    }
                  }
                }
            }

        var flattened = {};
        console.time('flatten'); 
        flatten(new_json, flattened, "");
        console.timeEnd('flatten');

        for (var key in flattened){
          console.log(key + ": " + flattened[key]);
        }

Output:

flatten: 1ms
.0.name: fatima
.0.age: 25
.0.neighbour.name: taqi
.0.neighbour.location: end of the street
.0.neighbour.property.built in: 1990
.0.neighbour.property.owned: false
.0.neighbour.property.years on market.0: 1990
.0.neighbour.property.years on market.1: 1998
.0.neighbour.property.years on market.2: 2002
.0.neighbour.property.years on market.3: 2013
.0.neighbour.property.year short listed: 
.0.town: Mountain View
.0.state: CA
.1.name: qianru
.1.age: 20
.1.neighbour.name: joe
.1.neighbour.location: opposite to the park
.1.neighbour.property.built in: 2011
.1.neighbour.property.owned: true
.1.neighbour.property.years on market.0: 1996
.1.neighbour.property.years on market.1: 2011
.1.neighbour.property.year short listed: 
.1.town: Pittsburgh
.1.state: PA
Dr. Banana
  • 435
  • 1
  • 7
  • 16
sfrizvi6
  • 37
  • 6
  • 1
    I think, that `typeof some === 'object'` is faster then `some instanceof Object` since the first check performs in O1 while second in On where n is a length of an inheritance chain (Object will always be the last one there). – GullerYA Apr 01 '16 at 11:20
2

Here's mine. It runs in <2ms in Google Apps Script on a sizable object. It uses dashes instead of dots for separators, and it doesn't handle arrays specially like in the asker's question, but this is what I wanted for my use.

function flatten (obj) {
  var newObj = {};
  for (var key in obj) {
    if (typeof obj[key] === 'object' && obj[key] !== null) {
      var temp = flatten(obj[key])
      for (var key2 in temp) {
        newObj[key+"-"+key2] = temp[key2];
      }
    } else {
      newObj[key] = obj[key];
    }
  }
  return newObj;
}

Example:

var test = {
  a: 1,
  b: 2,
  c: {
    c1: 3.1,
    c2: 3.2
  },
  d: 4,
  e: {
    e1: 5.1,
    e2: 5.2,
    e3: {
      e3a: 5.31,
      e3b: 5.32
    },
    e4: 5.4
  },
  f: 6
}

Logger.log("start");
Logger.log(JSON.stringify(flatten(test),null,2));
Logger.log("done");

Example output:

[17-02-08 13:21:05:245 CST] start
[17-02-08 13:21:05:246 CST] {
  "a": 1,
  "b": 2,
  "c-c1": 3.1,
  "c-c2": 3.2,
  "d": 4,
  "e-e1": 5.1,
  "e-e2": 5.2,
  "e-e3-e3a": 5.31,
  "e-e3-e3b": 5.32,
  "e-e4": 5.4,
  "f": 6
}
[17-02-08 13:21:05:247 CST] done
paulwal222
  • 1,488
  • 11
  • 18
2

Object.prototype.flatten = function (obj) {

    let ans = {};
    let anotherObj = { ...obj };
    function performFlatten(anotherObj) {

        Object.keys(anotherObj).forEach((key, idx) => {
            if (typeof anotherObj[key] !== 'object') {
                ans[key] = anotherObj[key];
                console.log('ans so far : ', ans);
            } else {
                console.log(key, { ...anotherObj[key] });
                performFlatten(anotherObj[key]);
            }
        })
    }

    performFlatten(anotherObj);

    return ans;
}

let ans = flatten(obj);
console.log(ans);
Aziza Kasenova
  • 1,501
  • 2
  • 10
  • 22
1

I added +/- 10-15% efficiency to the selected answer by minor code refactoring and moving the recursive function outside of the function namespace.

See my question: Are namespaced functions reevaluated on every call? for why this slows nested functions down.

function _flatten (target, obj, path) {
  var i, empty;
  if (obj.constructor === Object) {
    empty = true;
    for (i in obj) {
      empty = false;
      _flatten(target, obj[i], path ? path + '.' + i : i);
    }
    if (empty && path) {
      target[path] = {};
    }
  } 
  else if (obj.constructor === Array) {
    i = obj.length;
    if (i > 0) {
      while (i--) {
        _flatten(target, obj[i], path + '[' + i + ']');
      }
    } else {
      target[path] = [];
    }
  }
  else {
    target[path] = obj;
  }
}

function flatten (data) {
  var result = {};
  _flatten(result, data, null);
  return result;
}

See benchmark.

Community
  • 1
  • 1
jtrumbull
  • 818
  • 9
  • 19
1

Here's a recursive solution for flatten I put together in PowerShell:

#---helper function for ConvertTo-JhcUtilJsonTable
#
function getNodes {
    param (
        [Parameter(Mandatory)]
        [System.Object]
        $job,
        [Parameter(Mandatory)]
        [System.String]
        $path
    )

    $t = $job.GetType()
    $ct = 0
    $h = @{}

    if ($t.Name -eq 'PSCustomObject') {
        foreach ($m in Get-Member -InputObject $job -MemberType NoteProperty) {
            getNodes -job $job.($m.Name) -path ($path + '.' + $m.Name)
        }
        
    }
    elseif ($t.Name -eq 'Object[]') {
        foreach ($o in $job) {
            getNodes -job $o -path ($path + "[$ct]")
            $ct++
        }
    }
    else {
        $h[$path] = $job
        $h
    }
}


#---flattens a JSON document object into a key value table where keys are proper JSON paths corresponding to their value
#
function ConvertTo-JhcUtilJsonTable {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.Object[]]
        $jsonObj
    )

    begin {
        $rootNode = 'root'    
    }
    
    process {
        foreach ($o in $jsonObj) {
            $table = getNodes -job $o -path $rootNode

            # $h = @{}
            $a = @()
            $pat = '^' + $rootNode
            
            foreach ($i in $table) {
                foreach ($k in $i.keys) {
                    # $h[$k -replace $pat, ''] = $i[$k]
                    $a += New-Object -TypeName psobject -Property @{'Key' = $($k -replace $pat, ''); 'Value' = $i[$k]}
                    # $h[$k -replace $pat, ''] = $i[$k]
                }
            }
            # $h
            $a
        }
    }

    end{}
}

Example:

'{"name": "John","Address": {"house": "1234", "Street": "Boogie Ave"}, "pets": [{"Type": "Dog", "Age": 4, "Toys": ["rubberBall", "rope"]},{"Type": "Cat", "Age": 7, "Toys": ["catNip"]}]}' | ConvertFrom-Json | ConvertTo-JhcUtilJsonTable
Key              Value
---              -----
.Address.house   1234
.Address.Street  Boogie Ave
.name            John
.pets[0].Age     4
.pets[0].Toys[0] rubberBall
.pets[0].Toys[1] rope
.pets[0].Type    Dog
.pets[1].Age     7
.pets[1].Toys[0] catNip
.pets[1].Type    Cat
jCravener
  • 21
  • 3
1

I wanted an approach so that I could be able to easily convert my json data into a csv file. The scenario is: I query data from somewhere and I receive an array of some model, like a bank extract. This approach below is used to parse each one of these entries.

function jsonFlatter(data, previousKey, obj) {
    obj = obj || {}
    previousKey = previousKey || ""
    Object.keys(data).map(key => {
        let newKey = `${previousKey}${previousKey ? "_" : ""}${key}`
        let _value = data[key]
        let isArray = Array.isArray(_value)
        if (typeof _value !== "object" || isArray || _value == null) {
            if (isArray) {
                _value = JSON.stringify(_value)
            } else if (_value == null) {
                _value = "null"
            }
            obj[newKey] = _value
        } else if (typeof _value === "object") {
            if (!Object.keys(_value).length) {
                obj[newKey] = "null"
            } else {
                return jsonFlatter(_value, newKey, obj)
            }
        }
    })
    return obj
}

This way, I can count on the uniformity of the keys and inner keys of my object model, but arrays are simply stringified since I can't rely on their uniformity. Also, empty objects become the string "null", since I still want it's key to appear in the final result.

Usage example:

const test_data = {
    a: {
        aa: {
            aaa: 4354,
            aab: 654
        },
        ab: 123
    },
    b: 234,
    c: {},
    d: []
}

console.log('result', jsonFlatter(test_data)) 

#### output
{
  "a_aa_aaa": 4354,
  "a_aa_aab": 654,
  "a_ab": 123,
  "b": 234,
  "c": "null",
  "d": "[]"
}
Teodoro
  • 1,194
  • 8
  • 22
1

try this one:

    function getFlattenObject(data, response = {}) {
  for (const key in data) {
    if (typeof data[key] === 'object' && !Array.isArray(data[key])) {
      getFlattenObject(data[key], response);
    } else {
      response[key] = data[key];
    }
  }
  return response;
}
Anurag Sindhu
  • 25
  • 1
  • 5
0

I'd like to add a new version of flatten case (this is what i needed :)) which, according to my probes with the above jsFiddler, is slightly faster then the currently selected one. Moreover, me personally see this snippet a bit more readable, which is of course important for multi-developer projects.

function flattenObject(graph) {
    let result = {},
        item,
        key;

    function recurr(graph, path) {
        if (Array.isArray(graph)) {
            graph.forEach(function (itm, idx) {
                key = path + '[' + idx + ']';
                if (itm && typeof itm === 'object') {
                    recurr(itm, key);
                } else {
                    result[key] = itm;
                }
            });
        } else {
            Reflect.ownKeys(graph).forEach(function (p) {
                key = path + '.' + p;
                item = graph[p];
                if (item && typeof item === 'object') {
                    recurr(item, key);
                } else {
                    result[key] = item;
                }
            });
        }
    }
    recurr(graph, '');

    return result;
}
GullerYA
  • 1,320
  • 14
  • 27
  • what about unflatten? – Narxx Oct 11 '22 at 08:32
  • As I've mentioned in the answer I have not had that use case in the relevant project (nor unflatten is needed there even now). – GullerYA Oct 20 '22 at 19:05
  • I'm only asking because the post was asking about both Flatten and Unflatten, so for this answer to be completely valid, one would expect to cover all cases. – Narxx Oct 22 '22 at 16:54
0

Here is some code I wrote to flatten an object I was working with. It creates a new class that takes every nested field and brings it into the first layer. You could modify it to unflatten by remembering the original placement of the keys. It also assumes the keys are unique even across nested objects. Hope it helps.

class JSONFlattener {
    ojson = {}
    flattenedjson = {}

    constructor(original_json) {
        this.ojson = original_json
        this.flattenedjson = {}
        this.flatten()
    }

    flatten() {
        Object.keys(this.ojson).forEach(function(key){
            if (this.ojson[key] == null) {

            } else if (this.ojson[key].constructor == ({}).constructor) {
                this.combine(new JSONFlattener(this.ojson[key]).returnJSON())
            } else {
                this.flattenedjson[key] = this.ojson[key]
            }
        }, this)        
    }

    combine(new_json) {
        //assumes new_json is a flat array
        Object.keys(new_json).forEach(function(key){
            if (!this.flattenedjson.hasOwnProperty(key)) {
                this.flattenedjson[key] = new_json[key]
            } else {
                console.log(key+" is a duplicate key")
            }
        }, this)
    }

    returnJSON() {
        return this.flattenedjson
    }
}

console.log(new JSONFlattener(dad_dictionary).returnJSON())

As an example, it converts

nested_json = {
    "a": {
        "b": {
            "c": {
                "d": {
                    "a": 0
                }
            }
        }
    },
    "z": {
        "b":1
    },
    "d": {
        "c": {
            "c": 2
        }
    }
}

into

{ a: 0, b: 1, c: 2 }

Imran Q
  • 155
  • 1
  • 5
0

You can try out the package jpflat.

It flattens, inflates, resolves promises, flattens arrays, has customizable path creation and customizable value serialization.

The reducers and serializers receive the whole path as an array of it's parts, so more complex operations can be done to the path instead of modifying a single key or changing the delimiter.

Json path is the default, hence "jp"flat.

https://www.npmjs.com/package/jpflat

let flatFoo = await require('jpflat').flatten(foo)
Snowbldr
  • 776
  • 6
  • 11
0

The following is not a generic solution.

If you know the shape of the unflattened object you can write a simple custom flatten method in one line:

const o = {
  a:2,
  b: {
    c:3
  }, 
  d: 4
}

const f = ({b:c, ...rest}) => ({...c, ...rest})

f(o) // {c: 3, a: 2, d: 4}
Jeff Lowery
  • 2,492
  • 2
  • 32
  • 40