Skip to the code
cf.

Camelize keys

Many APIs send keys in snake_case, like user_id, while JavaScript code uses camelCase, like userId. Write camelizeKeys(value), which returns a copy of value with every object key converted, at every depth.

To convert a key, remove each underscore that's followed by a letter or a digit, and make that character uppercase: created_at becomes createdAt, and avatar_url_2x becomes avatarUrl2x. Every other character stays as it is, so user_ID becomes userID.

A key that starts with an underscore, like _id or __typename, is a special name: keep that key exactly as it is. Its value is still converted.

Walk into arrays and plain objects (the kind {} and JSON.parse make), including objects inside arrays. Anything else, such as a string, a date, or a class instance, is a value and stays as it is: only keys change.

Don't change value: every plain object and array in the result is new.

camelizeKeys({
  user_id: 7,
  display_name: "ada_l",
  _links: { self_url: "/users/7" },
  recent_posts: [{ post_id: 1, is_draft: false }],
});
// {
//   userId: 7,
//   displayName: "ada_l",
//   _links: { selfUrl: "/users/7" },
//   recentPosts: [{ postId: 1, isDraft: false }],
// }

What the tests check

  1. converts snake_case keys to camelCase
    expect(camelizeKeys({ user_id: 1, created_at: 2, is_two_factor_on: true })).toEqual({ userId: 1, createdAt: 2, isTwoFactorOn: true });
    
  2. converts every underscore before a letter or digit, even in one-letter parts
    expect(camelizeKeys({ a_b_c: 1, pos_x_y: 2, avatar_url_2x: 3 })).toEqual({ aBC: 1, posXY: 2, avatarUrl2x: 3 });
    
  3. keeps every other character as it is
    expect(camelizeKeys({ userId: 1, user_ID: 2, HTML_body: 3, name: 4 })).toEqual({ userId: 1, userID: 2, HTMLBody: 3, name: 4 });
    
  4. keeps keys that start with an underscore, but converts their values
    expect(camelizeKeys({ _id: 1, __typename: "User", _links: { self_url: "/u/1" } })).toEqual({
      _id: 1,
      __typename: "User",
      _links: { selfUrl: "/u/1" },
    });
    
  5. converts keys at every depth, inside arrays too
    expect(camelizeKeys({ user_profile: { home_address: { zip_code: "N1" } }, order_items: [{ item_id: 1 }, [{ unit_price: 2 }]] })).toEqual({
      userProfile: { homeAddress: { zipCode: "N1" } },
      orderItems: [{ itemId: 1 }, [{ unitPrice: 2 }]],
    });
    expect(camelizeKeys([{ first_name: "Ada" }])).toEqual([{ firstName: "Ada" }]);
    
  6. changes keys only, never values
    expect(camelizeKeys({ display_name: "ada_lovelace", tags: ["snake_case"], count: 0, empty_value: null })).toEqual({
      displayName: "ada_lovelace",
      tags: ["snake_case"],
      count: 0,
      emptyValue: null,
    });
    expect(camelizeKeys("some_text")).toBe("some_text");
    expect(camelizeKeys(null)).toBeNull();
    
  7. keeps dates and class instances as they are
    class Point {
      constructor() {
        this.x_pos = 1;
      }
    }
    const when = new Date(0);
    const at = new Point();
    const result = camelizeKeys({ created_at: when, start_point: at });
    expect(result.createdAt).toBe(when);
    expect(result.startPoint).toBe(at);
    expect(Object.keys(at)).toEqual(["x_pos"]);
    
  8. returns new objects and arrays and leaves the input alone
    const input = { outer_key: { inner_key: [1] }, name: { first: "Ada" } };
    const result = camelizeKeys(input);
    expect(result).not.toBe(input);
    expect(result.outerKey).not.toBe(input.outer_key);
    expect(result.outerKey.innerKey).not.toBe(input.outer_key.inner_key);
    expect(result.name).not.toBe(input.name);
    expect(input).toEqual({ outer_key: { inner_key: [1] }, name: { first: "Ada" } });
    

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