Skip to the code
cf.

Unflatten an object

Write unflattenObject(object), the reverse of flattening. Each key of object is a dotted path, and the new object you return has each value at its path.

unflattenObject({ "user.name": "Ada", "user.tags.0": "math", "user.tags.1": "code", active: true });
// { user: { name: "Ada", tags: ["math", "code"] }, active: true }

Go through the entries in order. When a step along a path is missing or isn't an object, put a new container there: an array if the next key is an array index (digits only), a plain object otherwise. Values are used as they are, by reference.

Skip any entry whose path contains the key __proto__, constructor, or prototype, so a flat object from user input can't pollute Object.prototype. Don't change object.

What the tests check

  1. nests dotted keys
    expect(unflattenObject({ "a.b": 1, "a.c.d": 2, e: 3 })).toEqual({ a: { b: 1, c: { d: 2 } }, e: 3 });
    
  2. builds arrays for index keys
    expect(unflattenObject({ "list.0.name": "a", "list.1.name": "b" })).toEqual({ list: [{ name: "a" }, { name: "b" }] });
    
  3. only digits-only keys make arrays
    expect(unflattenObject({ "a.1x": 1, "b.-1": 2 })).toEqual({ a: { "1x": 1 }, b: { "-1": 2 } });
    
  4. uses values as they are, empty objects included
    const empty = {};
    const when = new Date(0);
    const result = unflattenObject({ "a.0": 1, b: empty, "c.when": when });
    expect(result).toEqual({ a: [1], b: {}, c: { when: new Date(0) } });
    expect(result.b).toBe(empty);
    expect(result.c.when).toBe(when);
    
  5. a later path replaces a value in its way
    expect(unflattenObject({ a: 1, "a.b": 2 })).toEqual({ a: { b: 2 } });
    expect(unflattenObject({ a: null, "a.0": "x" })).toEqual({ a: ["x"] });
    
  6. skips paths that could pollute prototypes
    try {
      const result = unflattenObject({
        "__proto__.polluted": 1,
        "constructor.prototype.polluted": 1,
        "a.__proto__.polluted": 1,
        ok: 2,
      });
      expect(({}).polluted).toBeUndefined();
      expect(result).toEqual({ ok: 2 });
    } finally {
      delete Object.prototype.polluted;
    }
    
  7. returns a new object and leaves the input alone
    const input = { "a.b": 1 };
    expect(unflattenObject(input)).not.toBe(input);
    expect(input).toEqual({ "a.b": 1 });
    expect(unflattenObject({})).toEqual({});
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).