
Research
/Security News
16 Malicious Firefox Extensions Steal Cryptocurrency Wallet Credentials
Socket found 16 malicious Firefox extensions designed to steal crypto wallet recovery phrases and private keys using cloned Rabby and OKX interfaces.
caseparser
Advanced tools
Convert strings and object keys to any case, whatever case they are in now, with the resulting keys inferred by TypeScript.
If you're looking for version 1.x.x, click here to see the docs.
allowSymbols, ignore, transform, separatornpm add caseparser # or: pnpm add caseparser / yarn add caseparser
deno add jsr:@nandomb/caseparser
import { toSnake } from '@nandomb/caseparser';
bunx jsr add @nandomb/caseparser
npx jsr add @nandomb/caseparser
The input doesn't need a specific or consistent shape. Every key below is in a different case, and one call converts all of them, deeply:
import { toCamel } from 'caseparser'; // ESM
// const { toCamel } = require('caseparser'); // CommonJS
const response = {
userId: 42, // camelCase
FirstName: 'Ada', // PascalCase
last_name: 'Lovelace', // snake_case
'birth-date': '1815-12-10', // kebab-case
ACCOUNT_STATUS: 'active', // UPPER_SNAKE_CASE
'Content-Type': 'application/json', // Train-Case
'Display Name': 'Countess', // Title Case
XMLHttpRequest: false, // acronym + word
$ref: '#/users/42', // symbol
'billing address': { // nested objects...
'Postal Code': '61105',
countryISO: 'US'
},
'recent.orders': [{ order_id: 1, TotalAmount: 99.9 }], // ...and objects inside arrays
tags: ['someTag', 'another_tag'] // values are never converted
};
toCamel(response);
// {
// userId: 42,
// firstName: 'Ada',
// lastName: 'Lovelace',
// birthDate: '1815-12-10',
// accountStatus: 'active',
// contentType: 'application/json',
// displayName: 'Countess',
// xmlHttpRequest: false,
// ref: '#/users/42',
// billingAddress: { postalCode: '61105', countryIso: 'US' },
// recentOrders: [{ orderId: 1, totalAmount: 99.9 }],
// tags: ['someTag', 'another_tag']
// }
Strings work the same way: toSnake('helloWorld'), toSnake('Hello World') and toSnake('HELLO-WORLD') all return 'hello_world'.
The input is never mutated: a new object is returned. A typical use is converting API payloads on the way in and out:
import { toCamel, toSnake } from 'caseparser';
const res = await fetch('/api/users/1');
const user = toCamel(await res.json()); // { firstName, lastName, ... }
await fetch('/api/users/1', {
method: 'PUT',
body: JSON.stringify(toSnake(user)), // back to { first_name, ... }
});
There is one function per target case, and each one accepts input in any case:
| Function | 'helloWorld' → | 'LAST_login_AT' → | Also known as |
|---|---|---|---|
toCamel | helloWorld | lastLoginAt | lowerCamelCase |
toPascal | HelloWorld | LastLoginAt | UpperCamelCase |
toSnake | hello_world | last_login_at | |
toKebab | hello-world | last-login-at | dash-case, param-case |
toUpperSnake | HELLO_WORLD | LAST_LOGIN_AT | CONSTANT_CASE, SCREAMING_SNAKE_CASE |
toUpperKebab | HELLO-WORLD | LAST-LOGIN-AT | COBOL-CASE, SCREAMING-KEBAB-CASE |
toTrain | Hello-World | Last-Login-At | Header-Case |
toTitle | Hello World | Last Login At | Capital Case |
toSentence | Hello world | Last login at | |
toPascalSnake | Hello_World | Last_Login_At | Ada_Case, Title_Snake_Case |
toLower | hello world | last login at | no case |
toUpper | HELLO WORLD | LAST LOGIN AT | |
toDot | hello.World | LAST.login.AT | dot.notation |
toPath | hello/World | LAST/login/AT | |
toSpace | hello World | LAST login AT |
toDot, toPath and toSpace only change the separator: they keep the original case of each word (upper, lower or mixed). To change it, see transform.
Every function takes an optional second argument.
allowSymbolsSymbols (ASCII punctuation such as $, @, # or %) are removed by default. allowSymbols: true keeps all of them, and an array keeps only the listed ones. It can be passed inside the options object, or directly as the second argument:
const data = { $ref: 1, '@type': 'User', 'discount%': 10 };
toCamel(data); // { ref: 1, type: 'User', discount: 10 }
toCamel(data, { allowSymbols: true }); // { $ref: 1, '@type': 'User', 'discount%': 10 }
toCamel(data, { allowSymbols: ['$'] }); // { $ref: 1, type: 'User', discount: 10 }
// shorthand
toCamel(data, true); // same as { allowSymbols: true }
toCamel(data, ['$']); // same as { allowSymbols: ['$'] }
A kept symbol sticks to the start of the next word: toPascal('$id', true) → '$Id'. See How keys are split into words for the details.
ignoreObject keys that are kept as they are, at any depth. Their values are still converted:
toCamel({ _id: 1, user_id: 2, extra_data: { inner_key: 3 } }, { ignore: ['_id', 'extra_data'] });
// { _id: 1, userId: 2, extra_data: { innerKey: 3 } }
Keys are matched exactly as they are in the input. The inferred type keeps the ignored keys too. It only applies to object keys: toSnake('userId', { ignore: ['userId'] }) still returns 'user_id'.
transformOnly for toDot and toPath, which keep the original case of each word by default. 'lowercase' or 'uppercase' changes the case of the whole result, object keys included:
toDot('LAST_login_AT'); // 'LAST.login.AT'
toDot('LAST_login_AT', { transform: 'lowercase' }); // 'last.login.at'
toPath('LAST_login_AT', { transform: 'uppercase' }); // 'LAST/LOGIN/AT'
toDot({ user_ID: 1, LAST_name: 'Doe' }, { transform: 'lowercase' });
// { 'user.id': 1, 'last.name': 'Doe' }
toDot('$UserId', { allowSymbols: true, transform: 'lowercase' }); // '$user.id'
toSpace has no transform: use toLower or toUpper instead. Don't chain functions for this (toUpper(toPath(...))): each one splits its input into words again, so / and . would be treated as separators or symbols.
separatorOnly for toPath. It joins the words with '/' by default, or with '\\':
toPath('users/profilePicture'); // 'users/profile/Picture'
toPath('users/profilePicture', { separator: '\\' }); // 'users\\profile\\Picture'
toPath('users\\profilePicture', { separator: '/' }); // 'users/profile/Picture'
In toPath, / and \ in the input also separate words, so paths can be converted from one separator to the other. In the other functions they are symbols.
The converted keys are inferred at the type level, so your editor autocompletes them and catches typos:
const user = toCamel({ first_name: 'John', 'Home Addresses': [{ POSTAL_CODE: '61105' }] });
// ^? { firstName: string; homeAddresses: { postalCode: string }[] }
user.firstName; // ✅
user.first_name; // ❌ Property 'first_name' does not exist
The options are part of the inference: toCamel({ $ref: 1 }, ['$']) is typed { $ref: number }, and toDot({ UserId: 1 }, { transform: 'lowercase' }) is typed { 'user.id': number }. When the options are only known at runtime (e.g. a boolean variable), the keys are typed as string.
Every function first splits the input into words, then joins them in the target case. A new word starts:
_, -, . or a space, which are removed. Repeated or leading separators are ignored: toSnake('__private_flag') → 'private_flag'helloWorld → hello, worldXMLHttpRequest → xml, http, request, and userID → user, id/ or \, only in toPath (in the other functions they are symbols)Digits stay attached to the previous word: toSnake('html5Parser') → 'html5_parser'.
A kept symbol sticks to the start of the next word, or to the end of the previous one when no word follows:
toSnake('$$hello$World$$hi'); // 'hello_world_hi'
toSnake('$$hello$World$$hi', true); // '$$hello_$world_$$hi'
toSnake('user@name', true); // 'user_@name'
toSnake('total%_count', true); // 'total%_count'
The inferred types follow the same rules.
{ userName: 'johnDoe' }, userName becomes user_name but 'johnDoe' is kept. Strings inside arrays are kept too.Date, Map, Set, class instances and objects without a prototype (Object.create(null)) are returned as they are (same reference), without converting their contents.toSnake('userID') → 'user_id', and back toCamel('user_id') → 'userId'.toDot, toPath and toSpace), so toCamel('X-API-Key') → 'xApiKey' and toCamel('First Name') → 'firstName'.toTitle('termsOfUse') → 'Terms Of Use'.RangeError: Maximum call stack size exceeded. JSON.parse accepts that depth, so limit it if the input is untrusted and the error is not handled.Type instantiation is excessively deep and possibly infinite. The runtime conversion has no limit.toCamel and toSnake takes 1.5 KB gzipped (0.8 KB in 5.1.0), for the character tables and the key cache that make them faster. The whole package is smaller than before (1.8 KB gzipped, 3.1 KB in 5.1.0), because 6.0 removed the deprecated functions.toX function splits words the same way (acronyms, digits, symbols, non-ASCII letters), even when a simpler rule would be faster for plain snake_case → camelCase keys.Median time to convert the same data to each case on Node.js 24 (Apple M3 Pro); lower is faster. Each library was measured in 3 rounds, in a different order each time, and the time is the mean of the 3 medians. In bold: the fastest, and any library tied with it (its results in the 3 rounds overlap with the fastest's). ❌: the library doesn't convert keys to that case.
Every user has the same keys, like the items of a real API response.
| Case | caseparser | caseparser@5.1.0 | change-case | camelcase-keys | snakecase-keys | humps | es-toolkit |
|---|---|---|---|---|---|---|---|
| camelCase | 59 µs | 979 µs | 969 µs | 271 µs | ❌ | 546 µs | 392 µs |
| PascalCase | 59 µs | 1.06 ms | 1.03 ms | 364 µs | ❌ | 637 µs | 506 µs |
| snake_case | 57 µs | 891 µs | 1.07 ms | ❌ | 1.38 ms | 400 µs | 380 µs |
| kebab-case | 58 µs | 916 µs | 1.09 ms | ❌ | 1.41 ms | 404 µs | 385 µs |
| UPPER_SNAKE | 58 µs | 950 µs | 1.13 ms | ❌ | ❌ | ❌ | 455 µs |
| UPPER-KEBAB | 58 µs | 947 µs | 1.15 ms | ❌ | ❌ | ❌ | ❌ |
| Train-Case | 58 µs | 1.10 ms | 1.19 ms | ❌ | ❌ | ❌ | ❌ |
| Title Case | 58 µs | 1.11 ms | 1.16 ms | ❌ | ❌ | ❌ | ❌ |
| Sentence case | 58 µs | 1.02 ms | 1.15 ms | ❌ | ❌ | ❌ | ❌ |
| Pascal_Snake | 58 µs | 1.10 ms | 1.17 ms | ❌ | ❌ | ❌ | ❌ |
| lower case | 57 µs | 893 µs | 1.08 ms | ❌ | 1.40 ms | 401 µs | ❌ |
| UPPER CASE | 58 µs | 962 µs | 1.15 ms | ❌ | ❌ | ❌ | ❌ |
| dot.case | 57 µs | 903 µs | 1.07 ms | ❌ | 1.40 ms | 404 µs | ❌ |
| path/case | 57 µs | 971 µs | 1.08 ms | ❌ | 1.41 ms | 406 µs | ❌ |
No key repeats, in the object or between calls, so this measures how fast each key is converted.
| Case | caseparser | caseparser@5.1.0 | change-case | camelcase-keys | snakecase-keys | humps | es-toolkit |
|---|---|---|---|---|---|---|---|
| camelCase | 379 µs | 1.46 ms | 796 µs | 1.11 ms | ❌ | 576 µs | 464 µs |
| PascalCase | 415 µs | 2.08 ms | 888 µs | 1.35 ms | ❌ | 648 µs | 516 µs |
| snake_case | 390 µs | 1.32 ms | 849 µs | ❌ | 921 µs | 444 µs | 404 µs |
| kebab-case | 381 µs | 1.98 ms | 909 µs | ❌ | 1.00 ms | 519 µs | 475 µs |
| UPPER_SNAKE | 439 µs | 2.01 ms | 956 µs | ❌ | ❌ | ❌ | 497 µs |
| UPPER-KEBAB | 502 µs | 1.42 ms | 987 µs | ❌ | ❌ | ❌ | ❌ |
| Train-Case | 448 µs | 1.54 ms | 1.00 ms | ❌ | ❌ | ❌ | ❌ |
| Title Case | 416 µs | 1.53 ms | 995 µs | ❌ | ❌ | ❌ | ❌ |
| Sentence case | 429 µs | 2.08 ms | 926 µs | ❌ | ❌ | ❌ | ❌ |
| Pascal_Snake | 438 µs | 991 µs | 1.00 ms | ❌ | ❌ | ❌ | ❌ |
| lower case | 441 µs | 1.47 ms | 949 µs | ❌ | 995 µs | 509 µs | ❌ |
| UPPER CASE | 460 µs | 1.49 ms | 987 µs | ❌ | ❌ | ❌ | ❌ |
| dot.case | 442 µs | 854 µs | 961 µs | ❌ | 1.00 ms | 515 µs | ❌ |
| path/case | 475 µs | 1.42 ms | 973 µs | ❌ | 1.01 ms | 521 µs | ❌ |
With repeated keys, caseparser is 5–25x faster than the others, because it converts each distinct key once per call. With unique keys every library converts every key, and caseparser is the fastest or tied: this scenario creates a lot of garbage, so the results change up to ~20% between rounds for every library. For a small object (8 keys) converted over and over, camelcase-keys is faster to camelCase (0.75 µs, against 0.96 µs), because it keeps converted keys between calls (see Design trade-offs); to snake_case, caseparser is the fastest (0.92 µs). For strings, caseparser is the fastest too (1.1 µs for 10 strings, against 1.4 µs for es-toolkit).
Heap used on Node.js, measured in a new process for each library:
| Library | Loading the library | One call: API response | One call: 1,000 unique keys | Kept after converting 256,000 keys |
|---|---|---|---|---|
| caseparser | 70 KB | 121 KB | 516 KB | 0 |
| caseparser@5.1.0 | 289 KB | 2,348 KB | 2,719 KB | 0 |
| change-case | 136 KB | 2,850 KB | 1,610 KB | 0 |
| camelcase-keys | 209 KB | 1,516 KB | 1,742 KB | 11,483 KB |
| humps | 62 KB | 1,183 KB | 759 KB | 0 |
| es-toolkit | 622 KB | 1,745 KB | 1,062 KB | 0 |
Loading is the heap used by importing the library, as the benchmarks do. One call is the memory allocated to convert to camelCase, including the result (62 KB and 48 KB). "Kept" is the memory still used after the calls: camelcase-keys keeps the keys it converted between calls (up to 2 × 100,000). 0 means under 10 KB, which is the noise of the measurement.
Date, Map and class instances as they are. The others turn some of them into broken copies or plain objects.object, and change-case as unknown.__proto__ in untrusted JSON, humps.decamelizeKeys replaces the result's prototype, and with a constructor key, snakecase-keys throws. The others, caseparser included, convert both.ignore), camelcase-keys, snakecase-keys and humps have an option to keep some keys as they are; change-case and es-toolkit don't.userID → user_id or user_i_d, and html5Parser → html5_parser or html_5_parser. See what each library returns.The benchmarks check that every library returns the expected output before measuring it, and each library is called in its own file, with its documented options. The method and every result, with the raw numbers of every round, the date and the versions, are in bench/. To run them all:
pnpm install
pnpm benchmark
| Environment | Supported |
|---|---|
ESM (import) | Node.js 22+, Deno, Bun, bundlers |
CommonJS (require) | Node.js 22+, Bun |
| TypeScript | 4.7+ (any moduleResolution: node, node16/nodenext, bundler) |
| Browsers | Any ES2015 browser (via bundler) |
| Edge | Cloudflare Workers (example) |
On every change, CI runs the test suite on Node.js 22, 24 and 26, Bun and Deno, and in Chromium, Firefox and WebKit, and runs a Cloudflare Worker on workerd with the packed package. It also checks the packed package with publint and Are the Types Wrong?, for ESM, CommonJS and every moduleResolution, and compiles the type declarations with TypeScript 4.7.
caseparser is safe to use with untrusted input (e.g. request bodies or JSON.parse output): keys such as __proto__ are copied as regular keys and never change an object's prototype, a constructor key doesn't stop an object from being converted, only the object's own properties are converted, and nothing is kept between calls. Limit the depth of untrusted input if a RangeError for very deep objects is not handled (see Behavior and limitations).
Releases are built and published from GitHub Actions without long-lived tokens (OIDC), with npm provenance, so every published version can be traced back to the exact commit and workflow that built it. On npm, new versions are staged and only go live after a maintainer approves them with 2FA.
Found a vulnerability? Please report it privately, see SECURITY.md.
MIT © 2017 Fernando Machado Bernardino
FAQs
Convert Strings/Arrays/Objects from a type of case to another one.
The npm package caseparser receives a total of 930 weekly downloads. As such, caseparser popularity was classified as not popular.
We found that caseparser demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Research
/Security News
Socket found 16 malicious Firefox extensions designed to steal crypto wallet recovery phrases and private keys using cloned Rabby and OKX interfaces.

Product
Socket now scans VS Code extensions, giving teams early detection of risky behaviors, hidden capabilities, and supply chain threats in developer tools.

Research
/Security News
Socket uncovered two malicious VS Code themes in a GlassWorm-linked cluster with thousands of installs across VS Code Marketplace and Open VSX.