danday74
danday74

Reputation: 57006

One liner to flatten nested object

I need to flatten a nested object. Need a one liner. Not sure what the correct term for this process is. I can use pure Javascript or libraries, I particularly like underscore.

I've got ...

{
  a:2,
  b: {
    c:3
  }
}

And I want ...

{
  a:2,
  c:3
}

I've tried ...

var obj = {"fred":2,"jill":4,"obby":{"john":5}};
var resultObj = _.pick(obj, "fred")
alert(JSON.stringify(resultObj));

Which works but I also need this to work ...

var obj = {"fred":2,"jill":4,"obby":{"john":5}};
var resultObj = _.pick(obj, "john")
alert(JSON.stringify(resultObj));

Upvotes: 76

Views: 155521

Answers (20)

user8604852
user8604852

Reputation: 1

I like this code because it's a bit easier to understand.

Edit: I added some functionality I needed, so now it's a bit harder to understand.

const data = {
  a: "a",
  b: {
    c: "c",
    d: {
      e: "e",
      f: [
        "g",
        {
          i: "i",
          j: {},
          k: []
        }
      ]
    }
  }
};

function flatten(data, response = {}, flatKey = "", onlyLastKey = false) {
  for (const [key, value] of Object.entries(data)) {
    let newFlatKey;
    if (!isNaN(parseInt(key)) && flatKey.includes("[]")) {
      newFlatKey = (flatKey.charAt(flatKey.length - 1) == "." ? flatKey.slice(0, -1) : flatKey) + `[${key}]`;
    } else if (!flatKey.includes(".") && flatKey.length > 0) {
      newFlatKey = `${flatKey}.${key}`;
    } else {
      newFlatKey = `${flatKey}${key}`;
    }
    if (typeof value === "object" && value !== null && Object.keys(value).length > 0) {
      flatten(value, response, `${newFlatKey}.`, onlyLastKey);
    } else {
      if(onlyLastKey){
        newFlatKey = newFlatKey.split(".").pop();
      }
      if (Array.isArray(response)) {
        response.push({
          [newFlatKey.replace("[]", "")]: value
        });
      } else {
        response[newFlatKey.replace("[]", "")] = value;
      }
    }
  }
  return response;
}

console.log(flatten(data));
console.log(flatten(data, {}, "data"));
console.log(flatten(data, {}, "data[]"));
console.log(flatten(data, {}, "data", true));
console.log(flatten(data, {}, "data[]", true));
console.log(flatten(data, []));
console.log(flatten(data, [], "data"));
console.log(flatten(data, [], "data[]"));
console.log(flatten(data, [], "data", true));
console.log(flatten(data, [], "data[]", true));

Demo https://stackblitz.com/edit/typescript-flatter

For insinde a typescript class use:

function flatten(data: any, response = {}, flatKey = "", onlyLastKey = false) {
  for (const [key, value] of Object.entries(data)) {
    let newFlatKey: string;
    if (!isNaN(parseInt(key)) && flatKey.includes("[]")) {
      newFlatKey = (flatKey.charAt(flatKey.length - 1) == "." ? flatKey.slice(0, -1) : flatKey) + `[${key}]`;
    } else if (!flatKey.includes(".") && flatKey.length > 0) {
      newFlatKey = `${flatKey}.${key}`;
    } else {
      newFlatKey = `${flatKey}${key}`;
    }
    if (typeof value === "object" && value !== null && Object.keys(value).length > 0) {
      flatten(value, response, `${newFlatKey}.`, onlyLastKey);
    } else {
      if(onlyLastKey){
        newFlatKey = newFlatKey.split(".").pop();
      }
      if (Array.isArray(response)) {
        response.push({
          [newFlatKey.replace("[]", "")]: value
        });
      } else {
        response[newFlatKey.replace("[]", "")] = value;
      }
    }
  }
  return response;
}

Upvotes: 5

Fabien
Fabien

Reputation: 21

here is a simple typescript function which flatten object and concatenates keys:

function crushObj(
    rootObj:{[key: string]: any},
    obj: any,
    split = '/',
    prefix = ''
) {
    if (typeof obj === 'object') {
        for (const key of Object.keys(obj)) {
            const val = obj[key];
            delete obj[key];

            const rootKey = prefix.length > 0 ? `${prefix}${split}${key}` : key;

            crushObj(rootObj, val, split, rootKey)
        }
    }
    else {
        rootObj[prefix] = obj;
    }
}

You can use it like :

const obj = {
    name: 'John',
    address: {
      street: 'Aldo',
      number: 12,
    }
}
crushObj(obj, obj);

Results:

{
    name: "John",
    "address/street": "Aldo",
    "address/number": 12
}

Upvotes: 1

Andrew
Andrew

Reputation: 3969

Here is a flatten function that correctly outputs array indexes.

function flatten(obj) {
  const result = {};
  for (const key of Object.keys(obj)) {
    if (typeof obj[key] === 'object') {
      const nested = flatten(obj[key]);
      for (const nestedKey of Object.keys(nested)) {
        result[`${key}.${nestedKey}`] = nested[nestedKey];
      }
    } else {
      result[key] = obj[key];
    }
  }
  return result;
}

Example Input:

{
  "first_name": "validations.required",
  "no_middle_name": "validations.required",
  "last_name": "validations.required",
  "dob": "validations.required",
  "citizenship": "validations.required",
  "citizenship_identity": {
    "name": "validations.required",
    "value": "validations.required"
  },
  "address": [
    {
      "country_code": "validations.required",
      "street": "validations.required",
      "city": "validations.required",
      "state": "validations.required",
      "zipcode": "validations.required",
      "start_date": "validations.required",
      "end_date": "validations.required"
    },
    {
      "country_code": "validations.required",
      "street": "validations.required",
      "city": "validations.required",
      "state": "validations.required",
      "zipcode": "validations.required",
      "start_date": "validations.required",
      "end_date": "validations.required"
    }
  ]
}

Example Output:

const flattenedOutput = flatten(inputObj);
{
  "first_name": "validations.required",
  "no_middle_name": "validations.required",
  "last_name": "validations.required",
  "dob": "validations.required",
  "citizenship": "validations.required",
  "citizenship_identity.name": "validations.required",
  "citizenship_identity.value": "validations.required",
  "address.0.country_code": "validations.required",
  "address.0.street": "validations.required",
  "address.0.city": "validations.required",
  "address.0.state": "validations.required",
  "address.0.zipcode": "validations.required",
  "address.0.start_date": "validations.required",
  "address.0.end_date": "validations.required",
  "address.1.country_code": "validations.required",
  "address.1.street": "validations.required",
  "address.1.city": "validations.required",
  "address.1.state": "validations.required",
  "address.1.zipcode": "validations.required",
  "address.1.start_date": "validations.required",
  "address.1.end_date": "validations.required"
}

Upvotes: 10

novice_cplusplus
novice_cplusplus

Reputation: 101

Object.assign requires a polyfill. This version is similar to previous ones, but it is not using Object.assign and it is still keep tracking of parent's name

const flatten = (obj, parent = null) => Object.keys(obj).reduce((acc, cur) => 
    typeof obj[cur] === 'object' ? { ...acc, ...flatten(obj[cur], cur) } :
    { ...acc, [((parent) ? parent + '.' : "") + cur]: obj[cur] } , {})

const obj = {
  a:2,
  b: {
    c:3
  }
}

const flattened = flatten(obj)
console.log(flattened)

Upvotes: 2

metaory
metaory

Reputation: 188

ES6 Native, Recursive:

One-liner

const crushObj = (obj) => Object.keys(obj).reduce((acc, cur) => typeof obj[cur] === 'object' ? { ...acc, ...crushObj(obj[cur]) } : { ...acc, [cur]: obj[cur] } , {})

Expanded

const crushObj = (obj = {}) => Object.keys(obj || {}).reduce((acc, cur) => {
  if (typeof obj[cur] === 'object') {
    acc = { ...acc, ...crushObj(obj[cur])}
  } else { acc[cur] = obj[cur] }
  return acc
}, {})

Usage

const obj = {
  a:2,
  b: {
    c:3
  }
}

const crushed = crushObj(obj)
console.log(crushed)
// { a: 2, c: 3 }

Upvotes: 8

Neeraj Gupta
Neeraj Gupta

Reputation: 63

const obj = {
  a:2,
  b: {
    c:3
  }
}
// recursive function for extracting keys
function extractKeys(obj) {
  let flattenedObj = {};
  for(let [key, value] of Object.entries(obj)){
    if(typeof value === "object") {
      flattenedObj =  {...flattenedObj, ...extractKeys(value)};
    } else {
      flattenedObj[key] = value;
    }
  }
  return flattenedObj;
}
 
//  main code
let flattenedObj = extractKeys(obj);
console.log(flattenedObj);

Upvotes: 0

Webber
Webber

Reputation: 5494

Simplified readable example, no dependencies

/**
 * Flatten a multidimensional object
 *
 * For example:
 *   flattenObject{ a: 1, b: { c: 2 } }
 * Returns:
 *   { a: 1, c: 2}
 */
export const flattenObject = (obj) => {
  const flattened = {}

  Object.keys(obj).forEach((key) => {
    const value = obj[key]

    if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
      Object.assign(flattened, flattenObject(value))
    } else {
      flattened[key] = value
    }
  })

  return flattened
}

Features

Upvotes: 77

Julian
Julian

Reputation: 4366

Here is an actual oneliner of just 91 characters, using Underscore. (Of course. What else?)

var { reduce, isObject } = _;

var data = {
    a: 1,
    b: 2,
    c: {
        d: 3,
        e: 4,
        f: {
            g: 5
        },
        h: 6
    }
};

var tip = (v, m={}) => reduce(v, (m, v, k) => isObject(v) ? tip(v, m) : {...m, [k]: v}, m);

console.log(tip(data));
<script src="https://underscorejs.org/underscore-umd-min.js"></script>

Readable version:

var { reduce, isObject, extend } = _;

var data = {
    a: 1,
    b: 2,
    c: {
        d: 3,
        e: 4,
        f: {
            g: 5
        },
        h: 6
    }
};

// This function is passed to _.reduce below.
// We visit a single key of the input object. If the value
// itself is an object, we recursively copy its keys into
// the output object (memo) by calling tip. Otherwise we
// add the key-value pair to the output object directly.
function tipIteratee(memo, value, key) {
    if (isObject(value)) return tip(value, memo);
    return extend(memo, {[key]: value});
}

// The entry point of the algorithm. Walks over the keys of
// an object using _.reduce, collecting all tip keys in memo.
function tip(value, memo = {}) {
    return _.reduce(value, tipIteratee, memo);
}

console.log(tip(data));
<script src="https://underscorejs.org/underscore-umd-min.js"></script>

Also works with Lodash.

Upvotes: 3

Dennis Ameling
Dennis Ameling

Reputation: 737

Here's an ES6 version in TypeScript. It takes the best of answers given here and elsewhere. Some features:

  • Supports Date objects and converts them into ISO strings
  • Puts an underscore between the parent's and child's key (e.g. {a: {b: 'test'}} becomes {a_b: 'test'}
const flatten = (obj: Record<string, unknown>, parent?: string): Record<string, unknown> => {
    let res: Record<string, unknown> = {}

    for (const [key, value] of Object.entries(obj)) {
        const propName = parent ? parent + '_' + key : key
        const flattened: Record<string, unknown> = {}

        if (value instanceof Date) {
            flattened[key] = value.toISOString()
        } else if(typeof value === 'object' && value !== null){
            res = {...res, ...flatten(value as Record<string, unknown>, propName)}
        } else {
            res[propName] = value
        }
    }

    return res
}

An example:

const example = {
    person: {
        firstName: 'Demo',
        lastName: 'Person'
    },
    date: new Date(),
    hello: 'world'
}

// becomes

const flattenedExample = {
    person_firstName: 'Demo',
    person_lastName: 'Person',
    date: '2021-10-18T10:41:14.278Z',
    hello: 'world'
}

Upvotes: 5

muratgozel
muratgozel

Reputation: 2589

Here is a true, crazy one-liner that flats the nested object recursively:

const flatten = (obj, roots=[], sep='.') => Object.keys(obj).reduce((memo, prop) => Object.assign({}, memo, Object.prototype.toString.call(obj[prop]) === '[object Object]' ? flatten(obj[prop], roots.concat([prop]), sep) : {[roots.concat([prop]).join(sep)]: obj[prop]}), {})

Multiline version, explained:

// $roots keeps previous parent properties as they will be added as a prefix for each prop.
// $sep is just a preference if you want to seperate nested paths other than dot.
const flatten = (obj, roots = [], sep = '.') => Object
  // find props of given object
  .keys(obj)
  // return an object by iterating props
  .reduce((memo, prop) => Object.assign(
    // create a new object
    {},
    // include previously returned object
    memo,
    Object.prototype.toString.call(obj[prop]) === '[object Object]'
      // keep working if value is an object
      ? flatten(obj[prop], roots.concat([prop]), sep)
      // include current prop and value and prefix prop with the roots
      : {[roots.concat([prop]).join(sep)]: obj[prop]}
  ), {})

An example:

const obj = {a: 1, b: 'b', d: {dd: 'Y'}, e: {f: {g: 'g'}}}
const flat = flatten(obj)
{
  'a': 1, 
  'b': 'b', 
  'd.dd': 'Y', 
  'e.f.g': 'g'
}

Happy one-liner day!

Upvotes: 35

Marco Lackovic
Marco Lackovic

Reputation: 6497

My ES6 version:

const flatten = (obj) => {
    let res = {};
    for (const [key, value] of Object.entries(obj)) {
        if (typeof value === 'object') {
            res = { ...res, ...flatten(value) };
        } else {
            res[key] = value;
        }
    }
    return res;
}

Upvotes: 6

Web Dev
Web Dev

Reputation: 2937

Here's my TypeScript extension from @Webber's answer. Also supports dates:

private flattenObject(obj: any): any {
  const flattened = {};

  for (const key of Object.keys(obj)) {
    if (isNullOrUndefined(obj[key])) {
      continue;
    }

    if (typeof obj[key].getMonth === 'function') {
      flattened[key] = (obj[key] as Date).toISOString();
    } else if (typeof obj[key] === 'object' && obj[key] !== null) {
      Object.assign(flattened, this.flattenObject(obj[key]));
    } else {
      flattened[key] = obj[key];
    }
  }

  return flattened;
}

Upvotes: 1

dinesh dsv
dinesh dsv

Reputation: 13

I know its been very long, but it may be helpful for some one in the future

I've used recursion

let resObj = {};
function flattenObj(obj) {
    for (let key in obj) {
        if (!(typeof obj[key] == 'object')) {
            // console.log('not an object', key);
            resObj[key] = obj[key];
            // console.log('res obj is ', resObj);
        } else {
            flattenObj(obj[key]);
        }
    }

    return resObj;
}

Upvotes: 1

Johannes
Johannes

Reputation: 411

To flatten only the first level of the object and merge duplicate object keys into an array:

var myObj = {
  id: '123',
  props: {
    Name: 'Apple',
    Type: 'Fruit',
    Link: 'apple.com',
    id: '345'
  },
  moreprops: {
    id: "466"
  }
};

const flattenObject = (obj) => {
  let flat = {};
  for (const [key, value] of Object.entries(obj)) {
    if (typeof value === 'object' && value !== null) {
      for (const [subkey, subvalue] of Object.entries(value)) {
        // avoid overwriting duplicate keys: merge instead into array
        typeof flat[subkey] === 'undefined' ?
          flat[subkey] = subvalue :
          Array.isArray(flat[subkey]) ?
            flat[subkey].push(subvalue) :
            flat[subkey] = [flat[subkey], subvalue]
      }
    } else {
      flat = {...flat, ...{[key]: value}};
    }
  }
  return flat;
}

console.log(flattenObject(myObj))

Upvotes: 2

Ikenna Anthony Okafor
Ikenna Anthony Okafor

Reputation: 697

Here goes, not thoroughly tested. Utilizes ES6 syntax too!!

loopValues(val){
let vals = Object.values(val);
let q = [];
vals.forEach(elm => {
  if(elm === null || elm === undefined) { return; }
    if (typeof elm === 'object') {
      q = [...q, ...this.loopValues(elm)];
    }
    return q.push(elm);
  });
  return q;
}

let flatValues = this.loopValues(object)
flatValues = flatValues.filter(elm => typeof elm !== 'object');
console.log(flatValues);

Upvotes: 1

pkfm
pkfm

Reputation: 461

Here are vanilla solutions that work for arrays, primitives, regular expressions, functions, any number of nested object levels, and just about everything else I could throw at them. The first overwrites property values in the manner that you would expect from Object.assign.

((o) => {
  return o !== Object(o) || Array.isArray(o) ? {}
    : Object.assign({}, ...function leaves(o) {
    return [].concat.apply([], Object.entries(o)
      .map(([k, v]) => {
        return (( !v || typeof v !== 'object'
            || !Object.keys(v).some(key => v.hasOwnProperty(key))
            || Array.isArray(v))
          ? {[k]: v}
          : leaves(v)
        );
      })
    );
  }(o))
})(o)

The second accumulates values into an array.

((o) => {
  return o !== Object(o) || Array.isArray(o) ? {}
    : (function () {
      return Object.values((function leaves(o) {
        return [].concat.apply([], !o ? [] : Object.entries(o)
          .map(([k, v]) => {
            return (( !v || typeof v !== 'object'
                || !Object.keys(v).some(k => v.hasOwnProperty(k))
                || (Array.isArray(v) && !v.some(el => typeof el === 'object')))
              ? {[k]: v}
              : leaves(v)
            );
          })
        );
      }(o))).reduce((acc, cur) => {
        return ((key) => {
          acc[key] = !acc[key] ? [cur[key]]
            : new Array(...new Set(acc[key].concat([cur[key]])))
        })(Object.keys(cur)[0]) ? acc : acc
      }, {})
    })(o);
})(o)

Also please do not include code like this in production as it is terribly difficult to debug.

function leaves1(o) {
  return ((o) => {
    return o !== Object(o) || Array.isArray(o) ? {}
      : Object.assign({}, ...function leaves(o) {
      return [].concat.apply([], Object.entries(o)
        .map(([k, v]) => {
          return (( !v || typeof v !== 'object'
              || !Object.keys(v).some(key => v.hasOwnProperty(key))
              || Array.isArray(v))
            ? {[k]: v}
            : leaves(v)
          );
        })
      );
    }(o))
  })(o);
}

function leaves2(o) {
  return ((o) => {
    return o !== Object(o) || Array.isArray(o) ? {}
      : (function () {
        return Object.values((function leaves(o) {
          return [].concat.apply([], !o ? [] : Object.entries(o)
            .map(([k, v]) => {
              return (( !v || typeof v !== 'object'
                  || !Object.keys(v).some(k => v.hasOwnProperty(k))
                  || (Array.isArray(v) && !v.some(el => typeof el === 'object')))
                ? {[k]: v}
                : leaves(v)
              );
            })
          );
        }(o))).reduce((acc, cur) => {
          return ((key) => {
            acc[key] = !acc[key] ? [cur[key]]
              : new Array(...new Set(acc[key].concat([cur[key]])))
          })(Object.keys(cur)[0]) ? acc : acc
        }, {})
      })(o);
  })(o);
}

const obj = {
  l1k0: 'foo',
  l1k1: {
    l2k0: 'bar',
    l2k1: {
      l3k0: {},
      l3k1: null
    },
    l2k2: undefined
  },
  l1k2: 0,
  l2k3: {
    l3k2: true,
    l3k3: {
      l4k0: [1,2,3],
      l4k1: [4,5,'six', {7: 'eight'}],
      l4k2: {
        null: 'test',
        [{}]: 'obj',
        [Array.prototype.map]: Array.prototype.map,
        l5k3: ((o) => (typeof o === 'object'))(this.obj),
      }
    }
  },
  l1k4: '',
  l1k5: new RegExp(/[\s\t]+/g),
  l1k6: function(o) { return o.reduce((a,b) => a+b)},
  false: [],
}
const objs = [null, undefined, {}, [], ['non', 'empty'], 42, /[\s\t]+/g, obj];

objs.forEach(o => {
  console.log(leaves1(o));
});
objs.forEach(o => {
  console.log(leaves2(o));
});

Upvotes: 3

user663031
user663031

Reputation:

Here you go:

Object.assign({}, ...function _flatten(o) { return [].concat(...Object.keys(o).map(k => typeof o[k] === 'object' ? _flatten(o[k]) : ({[k]: o[k]})))}(yourObject))

Summary: recursively create an array of one-property objects, then combine them all with Object.assign.

This uses ES6 features including Object.assign or the spread operator, but it should be easy enough to rewrite not to require them.

For those who don't care about the one-line craziness and would prefer to be able to actually read it (depending on your definition of readability):

Object.assign(
  {}, 
  ...function _flatten(o) { 
    return [].concat(...Object.keys(o)
      .map(k => 
        typeof o[k] === 'object' ?
          _flatten(o[k]) : 
          ({[k]: o[k]})
      )
    );
  }(yourObject)
)

Upvotes: 91

Hau Le
Hau Le

Reputation: 121

function flatten(obj: any) {
  return Object.keys(obj).reduce((acc, current) => {
    const key = `${current}`;
    const currentValue = obj[current];
    if (Array.isArray(currentValue) || Object(currentValue) === currentValue) {
      Object.assign(acc, flatten(currentValue));
    } else {
      acc[key] = currentValue;
    }
    return acc;
  }, {});
};

let obj = {
  a:2,
  b: {
    c:3
  }
}

console.log(flatten(obj))

Demo https://stackblitz.com/edit/typescript-flatten-json

Upvotes: 1

James Brierley
James Brierley

Reputation: 4670

It's not quite a one liner, but here's a solution that doesn't require anything from ES6. It uses underscore's extend method, which could be swapped out for jQuery's.

function flatten(obj) {
    var flattenedObj = {};
    Object.keys(obj).forEach(function(key){
        if (typeof obj[key] === 'object') {
            $.extend(flattenedObj, flatten(obj[key]));
        } else {
            flattenedObj[key] = obj[key];
        }
    });
    return flattenedObj;    
}

Upvotes: 5

Sk93
Sk93

Reputation: 3718

This is a function I've got in my common libraries for exactly this purpose. I believe I got this from a similar stackoverflow question, but cannot remember which (edit: Fastest way to flatten / un-flatten nested JSON objects - Thanks Yoshi!)

function flatten(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;
}

This can then be called as follows:

var myJSON = '{a:2, b:{c:3}}';
var myFlattenedJSON = flatten(myJSON);

You can also append this function to the standard Javascript string class as follows:

String.prototype.flattenJSON = function() {
    var data = this;
    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;
}

With which, you can do the following:

var flattenedJSON = '{a:2, b:{c:3}}'.flattenJSON();

Upvotes: 3

Related Questions