Best way to flatten JS object (keys and values) to a single depth array

Viewed 134363

I have written this small function to get all keys and values of an object and store them into an array. The object might contain arrays as values...

Object { 0: [1,2,3,4] } to [0,1,2,3,4] converting all elements to integers

I wonder whether there is a faster/cleaner way to do so:

function flattenObject(obj) {
    // Returns array with all keys and values of an object
    var array = [];
    $.each(obj, function (key, value) {
        array.push(key);
        if ($.isArray(value)) {
            $.each(value, function (index, element) {
                array.push(element);
            });
        }
        else {
            array.push(value);
        }
    });

    return array
}
17 Answers

I wanted to flatten my deep object to one level depth. None of the above solutions worked for me.

My input:

{
    "user": {
        "key_value_map": {
            "CreatedDate": "123424",
            "Department": {
                "Name": "XYZ"
            }
        }
    }
}

Expected output:

{
    "user.key_value_map.CreatedDate": "123424",
    "user.key_value_map.Department.Name": "XYZ"
}

Code that worked for me:

function flattenObject(ob) {
    var toReturn = {};

    for (var i in ob) {
        if (!ob.hasOwnProperty(i)) continue;

        if ((typeof ob[i]) == 'object' && ob[i] !== null) {
            var flatObject = flattenObject(ob[i]);
            for (var x in flatObject) {
                if (!flatObject.hasOwnProperty(x)) continue;

                toReturn[i + '.' + x] = flatObject[x];
            }
        } else {
            toReturn[i] = ob[i];
        }
    }
    return toReturn;
}

Flattening Object can be done using recursion as below :

Sample Input

const obj = {
    name: "test",
    address: {
        personal: "abc",
        office: {
            building: 'random',
            street: 'some street'
        }
    }
}

Expected Output

{
    name : "test",
    address_personal: "abc"
    address_office_building: "random"
    address_office_street: "some street"
}


My Solution

  function flattenObj(obj, parent, res = {}){
    for(let key in obj){
        let propName = parent ? parent + '_' + key : key;
        if(typeof obj[key] == 'object'){
            flattenObj(obj[key], propName, res);
        } else {
            res[propName] = obj[key];
        }
    }
    return res;
}

Hope it helps

This answer is an improvement of @Muthukrishnan 's answer

If you want to flatten an object deeply outputting the values into a one level deep object keyed with the path of the value in the previous object

(eg: { foo: { bar: 'baz'} } => { 'foo.bar': 'baz' })

Here is how you can effectively do it:

/**
 * @param ob Object                 The object to flatten
 * @param prefix String (Optional)  The prefix to add before each key, also used for recursion
 **/
function flattenObject(ob, prefix = false, result = null) {
  result = result || {};

  // Preserve empty objects and arrays, they are lost otherwise
  if (prefix && typeof ob === 'object' && ob !== null && Object.keys(ob).length === 0) {
    result[prefix] = Array.isArray(ob) ? [] : {};
    return result;
  }

  prefix = prefix ? prefix + '.' : '';

  for (const i in ob) {
    if (Object.prototype.hasOwnProperty.call(ob, i)) {
      if (typeof ob[i] === 'object' && ob[i] !== null) {
        // Recursion on deeper objects
        flattenObject(ob[i], prefix + i, result);
      } else {
        result[prefix + i] = ob[i];
      }
    }
  }
  return result;
}

/**
 * Bonus function to unflatten an object
 *
 * @param ob Object     The object to unflatten
 */
function unflattenObject(ob) {
  const result = {};
  for (const i in ob) {
    if (Object.prototype.hasOwnProperty.call(ob, i)) {
      const keys = i.match(/(?:^\.+)?(?:\.{2,}|[^.])+(?:\.+$)?/g); // Just a complicated regex to only match a single dot in the middle of the string
      keys.reduce((r, e, j) => {
        return r[e] || (r[e] = isNaN(Number(keys[j + 1])) ? (keys.length - 1 === j ? ob[i] : {}) : []);
      }, result);
    }
  }
  return result;
}


// TESTS
const obj = {
  value: {
    foo: {
      bar: 'yes',
      so: {
        freakin: {
          nested: 'Wow',
        }
      }
    },
  },
  // Some edge cases to test
  test: [true, false, [null, undefined, 1]],
  not_lost: [], // Empty arrays should be preserved
  not_lost2: {}, // Empty objects should be preserved
  // Be careful with object having dots in the keys
  'I.like.dots..in.object.keys...': "... Please don't override me",
  I: {
    like: {
      'dots..in': {
        object: {
          'keys...': "You've been overwritten"
        }
      }
    }
  }
};
console.log(flattenObject(['I', {'am': 'an array'}]));
let flat = flattenObject(obj);
console.log(flat, unflattenObject(flat));

There is an obvious problem that you could encounter with flattening this way if your object contains keys with dots, this is documented in the fiddle

I needed something really simple and here is a one-liner I came up with:

function flatten(obj){
  return Object.values(obj).flat()
}

Obviously, this is subject to your browser/JS env supporting this syntax. Here is a working example.

const flatten=(obj)=>Object.values(obj).flat()

const x={x:[1,2,3],y:[4,5,6,7]}

console.log(flatten(x))

A more modern JavaScript and TypeScript implementation of a simple object to flat property map converter. It's using Object.entries to do a proper for of loop only on owned properties.

Exmaple Input:

const address = {
  name: 'Address 1',
  address: {
    street: {name: 'Test Street', no: 123}
  }
};

Output:

{
    'address.street.name': 'Test Street'
    'address.street.no': 123
    'name': 'Address 1'
}

JavaScript:

export function toFlatPropertyMap(obj, keySeparator = '.') {
  const flattenRecursive = (obj, parentProperty, propertyMap = {}) => {
    for(const [key, value] of Object.entries(obj)){
      const property = parentProperty ? `${parentProperty}${keySeparator}${key}` : key;
      if(value && typeof value === 'object'){
        flattenRecursive(value, property, propertyMap);
      } else {
        propertyMap[property] = value;
      }
    }
    return propertyMap;
  };
  return flattenRecursive(obj);
}

TypeScript:

export function toFlatPropertyMap(obj: object, keySeparator = '.') {
  const flattenRecursive = (obj: object, parentProperty?: string, propertyMap: Record<string, unknown> = {}) => {
    for(const [key, value] of Object.entries(obj)){
      const property = parentProperty ? `${parentProperty}${keySeparator}${key}` : key;
      if(value && typeof value === 'object'){
        flattenRecursive(value, property, propertyMap);
      } else {
        propertyMap[property] = value;
      }
    }
    return propertyMap;
  };
  return flattenRecursive(obj);
}

If you're feeling really lazy then you can make use of the popular NPM library flat.

Example (from their docs)

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
// }

I use this recursive function:

function flattenObject(obj, prefix = '') {
  return Object.keys(obj).reduce((acc, k) => {
    const pre = prefix.length ? prefix + '.' : '';
    if (typeof obj[k] === 'object') Object.assign(acc, flattenObject(obj[k], pre + k));
    else acc[pre + k] = obj[k];
    return acc;
  }, {});
}

Example of use:

const obj = { a: { b: { c: 1 } }, d: 1 };
const output = flattenObject(obj);
console.log(output); //{"a.b.c":1,"d":1}

This solution can handle deeply nested objects

const isObject = o => o && typeof o === 'object' && !(o instanceof Date);

const flattenObject = obj => Object.entries(obj).reduce((acc, [key, val]) => ({
  ...acc, ...(isObject(val) ? flattenObject(val) : { [key]: val })
}), {});

Remember that this function returns an empty object for strings, dates, numbers, etc.

Using Reduce in typescript It will be something like

export const flattenObject = (obj: Record<string, unknown>): Record<string, unknown> =>
  Object.entries(obj).reduce((acc, [key, value]) => {
    if (typeof value === 'object' && value !== null) {
      Object.entries(value).forEach(([iKey, iValue]) => {
        acc[`${key}-${iKey}`] = iValue;
      });
    } else {
      acc[key] = value;
    }
    return acc;
  }, {});

Using ES6 spread syntax:

function flattenObj(data, parent = null){
    // Create an empty object .
    let dataMap = {}
    // Loop over the data object that was given .
    for(const key in data){
        // Set a key name by checking if parent was set by previous recursive calls .
        const keyName = parent ? parent + '.' + key : key;
        // Check the data type.
        if(typeof data[key] === 'object' && !Array.isArray(data[key])) {
            // Using ES6 "Spread Operator" i overwrite the dataMap object with:
            // current dataMap + returned object result of the recurive call .
            dataMap = { ...dataMap, ...flattenObj(data[key], keyName)};
        } else {
            // If data type is anything but an object append the "key: value" .
            dataMap[keyName] = data[key];
        }
    }
    return dataMap;
}

The function below will flatten an object to the specified depth. This function uses a loop rather than recursion. You can choose how child property keys are named, the default is 'parent.child'. The result is an array of [key, value] arrays, like Object.entries(). It requires lodash for isPlainObject and partition(), though you could write your own isPlainObject, partition functions if you wanted to remove the dependency.

/**
 * Returns an array containing the properties of the given Object in the same format
 * as Object.entries(). Goes through child objects to the specified depth, 
 * flattening the properties and prefixing child keys with a parent key names.
 * @param {Object} object to retrieve property values for
 * @param {Number} maxDepth the maximum number of times to look at properties of
 * properties of the given object.
 * Set to 1 to only retrieve the property values of the given object, 2 to get
 * properties and sub-properties etc.
 * @param {Function} keyPrefixer a function that takes a parent object name, and
 * a child object name and returns a string representing the combined name.
 * @returns {Array} containing the properties and child properties of the given object.
 * Each property is returned as an array [key, value]. 
 * Returns an empty array if object is null, undefined, not-an-object, or empty.
 */
const flattenEntries = (
  object,
  maxDepth = 2,
  keyPrefixer = (parentKey, childKey) => `${parentKey}.${childKey}`) => {

  if (!object || !_.isPlainObject(object)) {
    return [];
  }

  // make maxDepth >= 1
  maxDepth = Math.max(1, Math.abs(maxDepth));

  const entryIsNotAnObject = ([key, val]) => !_.isPlainObject(val);

  let [simpleProperties, childObjects] = _.partition(Object.entries(object), entryIsNotAnObject);

  let result = simpleProperties;

  for (let depth = 1; depth < maxDepth; depth++) {

    for (let [childObjectKey, childObject] of childObjects) {
      const entries = Object.entries(childObject);
      const addParentPrefixToKey = ([key, val]) => [keyPrefixer(childObjectKey, key), val];
      const prefixedEntries = entries.map(addParentPrefixToKey);
      [simpleProperties, childObjects] = _.partition(prefixedEntries, entryIsNotAnObject);
      result = result.concat(simpleProperties);
    }
  }

  return result;
};

const test = {
  a: 'one',
  b: {
    c: 'three',
    d: {
      e: {
        f: ['six', 'six'],
        g: 7
      }
    }
  }
};

console.log(flattenEntries(test, 10));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>

The following solution handles the case for null values and arrays.

let user = {
  name: "John Doe",
  address: {
    personal: {
      city: "Haridwar",
      state: "Uttrakhand",
      area: "Majra",
    },
    office: {
      city: "Hyderabad",
      area: {
        landmark: "Hi Tech",
        pincode: [12321, 23414],
        lane: null
      }
    }
  }
}


function more_magic() {
  let ans = {};
  let magic = function (obj, parent) {
    for (let key in obj) {
      if (typeof obj[key] === "object" && obj[key] !== null && Array.isArray(obj[key]) === false) {
        magic(obj[key], parent + "_" + key);
      } else {
        ans[parent + "_" + key] = obj[key];
      }
    }
  }
  magic(user, "user");
  return ans;
}
console.log(more_magic())

I needed something similar that would deep flatten objects recursively into a string, but with customizations on different provided matchers. Based on some examples here this is what I finally ended up with, using lodash and some lodash fp functions. This way you can reduce true down to just "T" and undefined down to just "U". The matchers will need a matching key with the process. Everything else is just processed with String(item)

import _ from "lodash"

import flow from "lodash/fp/flow"
import some from "lodash/fp/some"
import reduce from "lodash/fp/reduce" 
....

const deepValuesToComparableString = items => {
  let matchers = {
    u: _.isUndefined, n: _.isNull,
    b: _.isBoolean, o: _.isObject,
    a: _.isArray, z: _.stubTrue
  }
  let process = {
    u: _.constant("U"), n: _.constant("N"),
    b: b => b ? "T" : "F", o: flow(_.flatMapDeep, _.values),
    a: _.flattenDeep, z: String
   }
  let convertForMatch = _.cond(_.zip(_.values(matchers), _.values(process)))
  let stillHasDepth = some(matchers.o || matchers.a)
  let valuesFor = reduce((acc, item) => [...acc, ...convertForMatch(item)], [])
  let flatReduceValues = reduce((acc, item) => [
    ...acc, ...stillHasDepth(item) 
      ? valuesFor(flatReduceValues(valuesFor(item))) 
      : valuesFor(item)
  ], [])
  return flatReduceValues(items).join("")
}

unit test:

test("it converts a 1d array of models into a string", () => {
  let someArrayData = [
    new TestDataClass({ someStr: "Test1", someOtherStr: "Abc", someNum: 1, someBool: false, someObjWithArrField: { someField: "some obj field", subRows: [{someSubRowField: "123", someOtherSubRowField: "testA"  }]}}),
    new TestDataClass({ someStr: "Test2", someOtherStr: undefined, someNum: 2, someBool: true,  someObjWithArrField: { someField: "obj field 2", subRows: [{someSubRowField: "234", someOtherSubRowField: "test B" }]}}),
    new TestDataClass({ someStr: "Sfds3", someOtherStr: "GGG", someNum: 3, someBool: null,  someObjWithArrField: { someField: "some field 3", subRows: [{someSubRowField: "456", someOtherSubRowField: "test C" }]}}),
  ]
  let result = deepValuesToComparableString(someArrayData)
  let expectedStr = "Test1Abc1Fsome obj field123testATest2U2Tobj field 2234test BSfds3GGG3Nsome field 3456test C"
  expect(result).toEqual(expectedStr)
})

Here is my TS implementation

Input:

const object = {
  topLevel: {
    numeric: 0,
    text: "string",
    bool: true,
    nested: {
      notDefined: undefined,
      nulll: null,
      array: [1, 2, 3]
    },
  },
};

const flat = flatten(object);

Expected result:

[
  "[topLevel][numeric]=0",
  "[topLevel][text]=string",
  "[topLevel][bool]=true",
  "[topLevel][nested][notDefined]=undefined",
  "[topLevel][nested][nulll]=null",
  "[topLevel][nested][array]=1,2,3",
]

Code:

export type InputValue = string | number | boolean | undefined | null;
export type ParsedValue = InputValue | InputValue[] | ParsedObject;
export type ParsedObject = { [key in string]: ParsedValue };

export const flatten = (object: ParsedObject, parent?: string): string[] => {
  let results: string[] = [];
  for (let key in object) {
    const value = object[key];
    const thisKey = parent ? `${parent}[${key}]` : `[${key}]`;

    results = results.concat(
      isObject(value) ? flatten(value, thisKey) : `${thisKey}=${value}`
    );
  }

  return results;
};

const isObject = (obj: ParsedValue): obj is ParsedObject => {
  return !!obj && typeof obj === "object" && !Array.isArray(obj);
};

And a JS snippet:

const flatten = (object, parent) => {
  let results = [];
  for (let key in object) {
    const value = object[key];
    const thisKey = parent ? `${parent}[${key}]` : `[${key}]`;

    results = results.concat(
      isObject(value) ? flatten(value, thisKey) : `${thisKey}=${value}`
    );
  }

  return results;
};

const isObject = (obj) => {
  return !!obj && typeof obj === "object" && !Array.isArray(obj);
};

console.log(
  flatten({
    topLevel: {
      numeric: 0,
      text: "string",
      bool: true,
      nested: { notDefined: undefined, nulll: null, array: [1, 2, 3] },
    },
  })
);

Related