Flatten a nested object
Flattening a nested object turns { user: { name: "Ada" } } into { "user.name": "Ada" }. Walk it recursively, carrying the path built so far, and write a value whenever you reach something that’s not a plain nested object.
The question
Common in data and configuration work, and usually asked with one deliberate ambiguity the
interviewer wants you to notice and raise: what counts as nested? Arrays and
null are both objects by typeof, and treating them as branches rather
than leaves changes the output completely.
The solution
The recursion carries the accumulated prefix rather than returning partial objects, which keeps it to one pass and one result object.
function squashObject(object) {
const result = {};
function walk(current, prefix) {
for (const key of Object.keys(current)) {
const value = current[key];
const path = prefix ? prefix + "." + key : key;
// Only plain nested objects are descended into; arrays and null are values.
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
walk(value, path);
} else {
result[path] = value;
}
}
}
walk(object, "");
return result;
}
How it works
- The prefix is threaded through the recursion, so the path is built on the way down and no post-processing is needed.
- Arrays are treated as leaf values, not descended into. That’s a decision, not an accident, and it’s exactly the thing to state out loud in an interview.
nullis checked explicitly, becausetypeof null === "object"would otherwise send the recursion into it.- The empty prefix case avoids a leading dot on top-level keys.
The tests it has to pass
This is the part most explanations leave out. "It works" is not a claim you can check by reading. These are the 2 cases Practice Pad runs against your solution, and the implementation above passes all of them.
✓ returns nested values under dot-separated paths
expectEqual(squashObject({ user: { name: "Ada" }, active: true }), {
"user.name": "Ada",
active: true,
});
✓ returns an empty object unchanged
expectEqual(squashObject({}), {});
Edge cases to watch out for
Producing a leading dot on top-level keys
Concatenating prefix + "." + key unconditionally yields ".active" at the top level. The ternary on the prefix is the fix.
Recursing into null
typeof null === "object", so an unguarded check tries to enumerate its keys. Object.keys(null) throws.
Descending into arrays without deciding to
If arrays are branches, { tags: ["a"] } becomes { "tags.0": "a" }. Either behaviour can be right; silently picking one without saying so is what loses marks.
Flattening empty objects out of existence
{ a: {} } produces nothing at all, because there are no leaves under a. Whether that’s correct depends on whether the result has to round-trip, which is worth asking about.
Follow up questions
Is there a built-in that does this?
No, and that’s why it keeps appearing in interviews. Lodash has no flattenObject either, though _.get and _.set understand the dot paths it produces. Form libraries and query-string encoders each ship their own version, which is a good sign the exact semantics are application-specific.
Should arrays be flattened too?
That’s the question to ask the interviewer rather than assume. Treating them as values keeps { tags: ["a", "b"] } intact; treating them as branches yields "tags.0" and "tags.1". Both are defensible, and naming the choice is what matters.
How would you reverse the flattening?
Split each key on the dot and walk down the result, creating an object at each missing level, then assign the value at the last segment. It only round-trips cleanly if no original key contained a dot, which is the main weakness of the format.
Practise this with the tests running
Practice Pad runs these 2 cases against your own solution, in a real editor, on your Mac. Twenty-four questions, and an optional AI panel that explains why a test failed.
Download for macOS Free. Requires macOS 12 or later on Apple Silicon.