Skip to content
LocalOnly
JSON Tools

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__.

Updated October 18, 20263 min readTool runs in your browser

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 a

Throwing 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.

Frequently asked questions

Command Palette

Search for a tool or command