Flatten a Nested Data Object
Analytics events, form payloads, and config files often arrive as deeply nested objects, but databases, query strings, and CSV exporters want a flat key-value map. This snippet builds a recursive flattener that produces dot-path keys, extends it to handle arrays with bracket notation, and adds the inverse `unflatten` so the round-trip is lossless. Drop it next to your event tracker or form serializer.
1,087 views
16
function flatten(obj, prefix = '', out = {}) {
for (const [key, value] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${key}` : key;
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
flatten(value, path, out);
} else {
out[path] = value;
}
}
return out;
}
const user = {
id: 7,
profile: {
name: 'Ada',
address: { city: 'NYC', zip: '10001' }
}
};
console.log(flatten(user));
// { id: 7, 'profile.name': 'Ada', 'profile.address.city': 'NYC', 'profile.address.zip': '10001' }The flattener walks every key, and when the value is a plain object it recurses with an extended prefix; otherwise it writes the leaf into out under the joined dot path. The accumulator out is threaded through so each recursive call appends to the same map, which avoids spreading dozens of intermediate objects on the way back up. The value !== null && typeof value === 'object' guard is critical because typeof null === 'object' in JavaScript, and a missed null check produces an infinite loop. This version intentionally treats arrays as leaves so the basic shape stays simple, accordion 2 lifts that restriction.
function flatten(obj, prefix = '', out = {}) {
const isPlainObject = v => v !== null && typeof v === 'object' && !Array.isArray(v);
const entries = Array.isArray(obj)
? obj.map((v, i) => [i, v])
: Object.entries(obj);
for (const [key, value] of entries) {
const path = Array.isArray(obj)
? `${prefix}[${key}]`
: prefix ? `${prefix}.${key}` : key;
if (isPlainObject(value) || Array.isArray(value)) {
flatten(value, path, out);
} else {
out[path] = value;
}
}
return out;
}
const event = {
type: 'order_placed',
items: [
{ sku: 'A1', qty: 2 },
{ sku: 'B7', qty: 1 }
]
};
console.log(flatten(event));
// { type: 'order_placed', 'items[0].sku': 'A1', 'items[0].qty': 2, 'items[1].sku': 'B7', 'items[1].qty': 1 }Real payloads almost always include arrays, and dropping them as opaque leaves loses information. Switching to obj.map((v, i) => [i, v]) for arrays lets the same recursion handle both shapes, and the path builder emits items[0].sku instead of items.0.sku so the output reads like the JSON you would write by hand. Notice that the recursive call still passes the array through, so nested arrays of arrays also work. Time and space are both O(n) in the number of leaves, with one allocation per intermediate path string.
function unflatten(flat) {
const out = {};
for (const [path, value] of Object.entries(flat)) {
const tokens = path.match(/[^.[\]]+/g) || [];
let cursor = out;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
const isLast = i === tokens.length - 1;
const nextToken = tokens[i + 1];
const nextIsIndex = nextToken !== undefined && /^\d+$/.test(nextToken);
if (isLast) {
cursor[token] = value;
} else {
if (cursor[token] === undefined) {
cursor[token] = nextIsIndex ? [] : {};
}
cursor = cursor[token];
}
}
}
return out;
}
const flat = {
type: 'order_placed',
'items[0].sku': 'A1',
'items[0].qty': 2,
'items[1].sku': 'B7'
};
console.log(JSON.stringify(unflatten(flat)));
// {"type":"order_placed","items":[{"sku":"A1","qty":2},{"sku":"B7"}]}Unflatten is the dual of accordion 2 and is what makes the encoding actually useful as a wire format. Each path is split into tokens by stripping ., [, and ], and the loop builds intermediate containers on the way down, peeking at the next token to decide whether to seed an array or an object. Numeric tokens land in the array slot you would expect, so items[0].sku = 'A1' and items[1].sku = 'B7' rebuild the original shape even when an inner field is missing. Use this pair when bridging between a flat store (form fields, query strings, CSV) and a structured object your code wants to consume.
