camelcase-keys: rewriting object keys to camel case, deeply if you ask
Convert object keys to camel case
At a glance
- What is it?
- camelcase-keys is a small TypeScript package that converts object keys to camel case, with options for deep recursion, Pascal case, preserved consecutive capitals and paths to leave untouched. It is the practical tool for the moment when an API starts sending kebab case keys into TypeScript code that expects camel case.
- Who is it for?
- Adopt camelcase-keys at the boundary where a foreign payload enters your code, since it does one transformation well and gives you the escape hatches you need, particularly stopPaths for keys that must keep their original shape. Skip it if you already control both sides of the interface, because a plain mapper is then simpler than a dependency.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 33 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One function, one transformation, and the case it came from
camelcase-keys converts object keys to camel case, and it does that using a separate package called camelcase. The whole surface is one exported function, camelcaseKeys, that takes an input and an optional options object.
The basic use is a plain object:
import camelcaseKeys from 'camelcase-keys';
// Convert an object
camelcaseKeys({'foo-bar': true});
//=> {fooBar: true}
// Convert an array of objects
camelcaseKeys([{'foo-bar': true}, {'bar-foo': false}]);
//=> [{fooBar: true}, {barFoo: false}]The input type is a Record of string to unknown, or a readonly array of such records, and the README is precise that these are plain objects or arrays of plain objects. That is a real constraint rather than a technicality, and it is worth understanding before you pass something in.
The case this solves is concrete. A command line parser is the example given in the usage section:
import {parseArgs} from 'node:util';
import camelcaseKeys from 'camelcase-keys';
const commandLineArguments = parseArgs();
//=> {_: [], 'foo-bar': true}
camelcaseKeys(commandLineArguments);
//=> {_: [], fooBar: true}Node's own parseArgs produces a bare _ key for positional arguments and keeps dashes in flag names, and the result is a shape that does not match the camel case convention most TypeScript code uses. The library sits between the parser and your code.
The package is MIT licensed, authored by Sindre Sorhus, version 10.0.3 in the manifest, with the last push on 2026-09-20 and the v10.0.3 release the same day.
The deep option, and why the default is false
The deep option controls whether nested objects and objects inside arrays are recursed into, and it defaults to false:
import camelcaseKeys from 'camelcase-keys';
const object = {
'foo-bar': true,
nested: {
unicorn_rainbow: true
}
};
camelcaseKeys(object, {deep: true});
//=> {fooBar: true, nested: {unicornRainbow: true}}
camelcaseKeys(object, {deep: false});
//=> {fooBar: true, nested: {unicorn_rainbow: true}}The shallow result is the interesting half. The top level key is converted, and the nested object is passed through untouched, with its own key left as it was. That is the safer default, because a deep traversal of an arbitrary payload can walk into structures you did not expect to find, such as a metadata blob whose keys are significant to another system.
The type signature reflects the option, and it is worth noting that the option list is short. Beyond deep there is exclude, which takes an array of strings or regular expressions and excludes those keys from being camel-cased, with the note that you should add as const to a string array for correct TypeScript types.
The README also names the inverse of this package, decamelize-keys, and lists snakecase-keys and kebabcase-keys as related projects. So if you need to go the other way, or to a different convention, the ecosystem around this package already exists rather than needing to be written.
Pascal case and the consecutive uppercase flag
Two options control the shape of the converted key beyond plain camel case.
The pascalCase option uppercases the first character, so bye-bye becomes ByeBye, and it defaults to false:
import camelcaseKeys from 'camelcase-keys';
camelcaseKeys({'foo-bar': true}, {pascalCase: true});
//=> {FooBar: true}
camelcaseKeys({'foo-bar': true}, {pascalCase: false});
//=> {fooBar: true}The preserveConsecutiveUppercase option keeps runs of capitals together, so foo-BAR becomes FooBAR, and it also defaults to false:
import camelcaseKeys from 'camelcase-keys';
camelcaseKeys({'foo-BAR': true}, {preserveConsecutiveUppercase: true});
//=> {fooBAR: true}
camelcaseKeys({'foo-BAR': true}, {preserveConsecutiveUppercase: false});
//=> {fooBar: true}The behaviour that matters is what happens when the option is off. With preserveConsecutiveUppercase set to false, the key foo-BAR becomes fooBar, with the run of capitals flattened into a single capitalised word. With it set to true, the run survives as fooBAR. This is a real difference for keys that come from an acronym, where losing the capitals changes what the key means to a reader.
The underlying camelcase package is where the actual conversion rules live, and it is pinned at version 9 or newer in the dependencies. The other runtime dependencies are map-obj at 6.0.0 for the key iteration, quick-lru at 7.3.0 for a small cache, and type-fest at 5.4.1 for the TypeScript types.
stopPaths lets you carve out a subtree, and it is the option that takes reading
The stopPaths option excludes children at given object paths from being camel-cased, and it only has an effect together with the deep option. The paths use dot notation and they use the input key casing, not the converted casing, so for an object like {a_b: {c_d: 'value'}} the path to reach the inner value is a_b.c_d.
The subtlety is that the key at a stopped path is still camel-cased. Only its children are left alone.
import camelcaseKeys from 'camelcase-keys';
const object = {
a_b: 1,
a_c: {
c_d: 1,
c_e: {
e_f: 1
}
}
};
camelcaseKeys(object, {
deep: true,
stopPaths: [
'a_c.c_e'
]
}),
/*
{
aB: 1,
aC: {
cD: 1,
cE: {
e_f: 1
}
}
}
*/So a_c becomes aC, c_d becomes cD, c_e becomes cE, and e_f stays e_f because it sits below the stopped path. The path is resolved against the original keys, which means you write the path in the shape the data arrives in, not the shape the output takes. That is a design decision that will confuse you once if you do not know it.
There is a second wrinkle with arrays. When an object is inside an array, the path is specified without array indices, and a stopPath applies to all items in the array:
import camelcaseKeys from 'camelcase-keys';
const object = {
foo: [
{
bar: {
baz_qux: 'value'
}
}
]
};As with exclude, the README asks for as const on the array for correct TypeScript types.
A leading _ and a leading $ are preserved, and the reason given is that they have semantic meaning. The example shows _foo_bar becoming _fooBar and $baz_qux becoming $bazQux, so the prefix survives while the rest of the key is still converted.
An ESM only package with its own types and benchmark
The package manifest is short and modern. The type is module, so the package is ESM only, and the exports map has just a types entry pointing at index.d.ts and a default entry pointing at index.js, with no require condition. On Node 20 and newer that is the whole story, since the engines field requires Node 20 or newer.
The files field lists only index.js and index.d.ts, so the shipped package is two files, and the repository root matches that with index.js, index.d.ts, a test.js, an index.test-d.ts, a bench directory, a fixtures directory, an .editorconfig, an .npmrc and a .gitattributes.
The tooling tells you how the project is tested. The test script runs xo, then ava, then tsd, which means linting, unit tests and type definition tests all run in one command. The bench script runs matcha against bench/bench.js, and the presence of a bench directory and a fixtures directory suggests the benchmark uses fixed inputs so the numbers are comparable across runs.
The dev dependencies match that: ava for the tests, tsd for the type tests, xo for the linting, and matcha for the benchmark. The xo configuration in the manifest turns off the unified-signatures rule from the typescript-eslint plugin, which is a small concession to the fact that the options object has overloads.
Two details are worth noticing. The sideEffects field is set to false, which lets a bundler drop the import entirely if nothing is used from it, and the type-fest dependency brings the TypeScript utility types the option objects rely on. The version history shows the 10.x line with 10.0.1 on 2025-11-02, 10.0.2 on 2026-01-25 and 10.0.3 on 2026-09-20.
Where camelcase-keys is the wrong tool
The first wrong case is when you own both ends of the interface. If the code producing the keys and the code consuming them are both yours, the right fix is to name the keys consistently at the source rather than to convert them on every read. A library that normalises keys at runtime hides the inconsistency instead of removing it, and the cost shows up in type definitions, which have to describe the converted shape rather than the shape that actually exists on the wire.
The second is a payload whose key shape is significant. The package explicitly preserves a leading _ character and a $ sign because those carry meaning, which is a hint about how often that matters. If keys in your payload are identifiers, not labels, converting them breaks the link between the object and the thing it describes, and stopPaths is a workaround rather than a fix.
The third is deep traversal over a large or unpredictable structure. The deep option walks nested objects and objects inside arrays, and on a payload with cycles or very deep nesting that is a runtime cost you did not budget for. The default of false is the safe choice for that reason, and stopPaths only helps if you know the paths in advance.
The fourth is a CommonJS codebase. The package is ESM only with no require export, so a project still on CommonJS cannot consume it without a bundler that handles ESM. That is a hard stop rather than a cost, and it is the same reason the engines field sits at Node 20.
The realistic alternative is writing the mapping yourself, which for a fixed set of keys is a few lines and zero dependencies, and which for a large or irregular payload is a chore the library exists to solve. The deciding question is whether your keys are known and few, or arbitrary and many.
Editorial conclusion
Adopt camelcase-keys at the boundary where a foreign payload enters your code, since it does one transformation well and gives you the escape hatches you need, particularly stopPaths for keys that must keep their original shape. Skip it if you already control both sides of the interface, because a plain mapper is then simpler than a dependency. Before you pin a version, note that the package requires Node 20 or newer, is ESM only with a type of module and no CommonJS export, and the last push was on 2026-09-20 with version 10.0.3 released the same day, so check whether the version you use still matches the option names you rely on.
Frequently asked questions
How do I install and use camelcase-keys?
Run npm install camelcase-keys, then import it and call camelcaseKeys on an object or an array of objects. A key like foo-bar becomes fooBar, and the package is ESM only with a type of module, so it requires Node 20 or newer and does not provide a CommonJS export.
How do I make camelcase-keys recurse into nested objects?
Pass the deep option set to true. It defaults to false, so without it the top level keys are converted and nested objects are passed through untouched, which is the safer default for payloads whose nested key shapes matter.
What does the stopPaths option do in camelcase-keys?
It excludes children at given dot-notation object paths from being camel-cased, and it only takes effect together with the deep option. The paths use the input key casing, the key at a stopped path is still converted, and when an object sits inside an array the path is written without indices and applies to every item.
How do I keep consecutive uppercase letters when converting keys?
Set preserveConsecutiveUppercase to true, and a key like foo-BAR becomes fooBAR instead of the fooBar you get by default. The pascalCase option separately controls whether the first character of the converted key is uppercased, and it also defaults to false.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sindresorhus-camelcase-keys)Was this useful?