How to Unflatten JSON: Dot Notation to Nested Objects
Turning user.address.city back into nested objects takes about fifteen lines. Here is a version that builds arrays, fails clearly on conflicting keys and ignores __proto__.
To unflatten JSON, you turn flat keys back into nested objects. Flat keys such as user.address.city or tags[0] come from spreadsheet columns, form fields, query strings and config files. The JSON Unflattener rebuilds the nested document in the browser, and its page explains the ambiguities involved. This guide gives you the same operation as a function you can use in code.
Unflatten JSON in JavaScript
function unflatten(flat) {
const result = {};
for (const [path, value] of Object.entries(flat)) {
const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
if (parts.includes("__proto__")) continue;
let node = result;
for (const [i, part] of parts.entries()) {
const isLast = i === parts.length - 1;
if (isLast && part in node) throw new Error("Conflicting keys at " + path);
if (isLast) node[part] = value;
else {
node[part] ??= /^\d+$/.test(parts[i + 1]) ? [] : {};
if (typeof node[part] !== "object") throw new Error("Conflicting keys at " + path);
node = node[part];
}
}
}
return result;
}
unflatten({ "user.name": "Ada", "user.address.city": "Oslo", "tags[0]": "a", "tags[1]": "b" });
// { user: { name: "Ada", address: { city: "Oslo" } }, tags: ["a", "b"] }Each key is split into parts. Bracket indexes are first rewritten as dots, so tags[0] becomes tags.0. The function then walks down the result, creating containers as it goes: an array when the next part is a number, an object otherwise.
Failing clearly on conflicting keys
A flat map can describe something impossible. {"a": 1, "a.b": 2} asks for a to be both the number 1 and an object. A naive version crashes with Cannot create property 'b' on number '1', or, with the keys in the other order, silently replaces the object with the number. The two checks in the function turn both cases into a clear error:
{"a": 1, "a.b": 2} -> Error: Conflicting keys at a.b
{"a.b": 2, "a": 1} -> Error: Conflicting keys at aThrowing is the right default in code, because a conflict nearly always means the input was built wrongly. If you prefer the last key to win, remove the two checks, but then the result depends on key order.
Skipping unsafe keys
If the flat keys come from outside your program, someone can send __proto__.admin=true. A version without the __proto__ check would assign through to Object.prototype, and every object in the process would suddenly have admin set. This bug class is called prototype pollution. With the check, unflatten({"__proto__.admin": true}) returns {} and nothing else changes. Keep it whenever the input is not your own.
From form fields and query strings
HTML forms often use bracket names such as user[address][city]. Rewrite every bracket as a dot, then unflatten:
const query = "user[name]=Ada&user[address][city]=Oslo&tags[0]=a&tags[1]=b";
const flat = Object.fromEntries(
[...new URLSearchParams(query)].map(([name, value]) => [name.replace(/\[([^\]]+)\]/g, ".$1"), value]),
);
unflatten(flat);
// { user: { name: "Ada", address: { city: "Oslo" } }, tags: ["a", "b"] }Object.fromEntries keeps only the last value when a name repeats, as in ids=3&ids=7. If your form sends repeated names, collect those with getAll first. Many server frameworks can parse bracket names for you; check whether yours does before adding this step.
Related tools
Flatten nested JSON into single-level dot-notation key paths.
Parse CSV into a clean array of typed JSON objects, header-aware.
Pretty-print JSON with configurable indentation and instant validation.
Explore JSON as a collapsible, interactive tree.
Related guides
A recursive function of about ten lines flattens any JSON document into path keys. Here it is for JavaScript and Python, with the choices that decide whether you can rebuild the original.
A single quoted comma breaks the split approach. Here is correct CSV parsing in Python and JavaScript, the byte order mark that renames your first column, and safe type conversion.
Two formatters can disagree about the same JSON file. Here is how a team picks one, applies it in every editor and before every commit, and makes CI enforce it.
Loading a 121 MB JSON file took over a gigabyte of memory in jq. Streaming the same file took 3 MB. Here is how to explore large JSON without loading it all at once.