snake_case to camelCase: Don't Break Your Acronyms (2026)
Converting snake_case to camelCase is trivial until acronyms and digits arrive. The boundary rule, the round-trips that lose data, and where to convert.
snake_case to camelCase: How to Convert Without Breaking Acronyms
A Python service emits http_status_code, the TypeScript client wants httpStatusCode, and a one-line regex seems to settle it. Then oauth2_token arrives, and the same conversion round-trips into oauth_2_token. Converting snake_case to camelCase is easy for plain words and genuinely lossy for acronyms, digits and stray underscores. Here is where the boundaries are.
TL;DR
- Delete each underscore, uppercase the next character: that covers ordinary identifiers.
- snake_case already discarded capitalisation, so acronyms cannot be recovered — only guessed.
- Digits are the real hazard:
oauth2_tokenandipv6_addressdo not survive a round-trip. - Leading, trailing and doubled underscores are silently dropped by every converter.
- Convert at one boundary only; store one canonical form and generate the other.
How to convert snake_case to camelCase
The transformation is mechanical. Underscore is the word delimiter, so every underscore is removed and the letter after it is promoted to upper case. The first word stays lowercase for lowerCamelCase, or gets capitalised for UpperCamelCase — the same algorithm, one flag apart.
The one-line regex that covers most identifiers
const toCamel = (s) =>
s.replace(/_+([a-z0-9])/g, (_, c) => c.toUpperCase());
toCamel('user_id'); // 'userId'
toCamel('http_status_code'); // 'httpStatusCode'
toCamel('created_at'); // 'createdAt'
Two details in that pattern matter. The _+ quantifier collapses runs of underscores instead of leaving a stray one behind, and including 0-9 in the character class means x_0 becomes x0 rather than stopping at the digit. If you want PascalCase instead, capitalise the first character afterwards rather than complicating the regex.
For a one-off conversion — a column list, a config block, a set of API field names pasted out of a schema — running it through iKit's Case Converter is faster than opening a REPL, and nothing leaves the tab. If you want to see exactly what a pattern matches before committing to it across a codebase, the Regex Tester will show the capture groups live.
How to convert every key in a JSON object to camelCase
Field-name conversion almost never happens on a single string. It happens on an entire API payload, which means walking nested objects and arrays:
function camelKeys(value) {
if (Array.isArray(value))
return value.map(camelKeys);
if (value === null || typeof value !== 'object')
return value;
return Object.fromEntries(
Object.entries(value).map(
([k, v]) => [toCamel(k), camelKeys(v)]
)
);
}
Note what this deliberately does not do: it never touches values. A string value of "created_at" inside a payload is data, not an identifier, and rewriting it corrupts the record. Key-only transformation is the rule. If you are unsure what the shape of an incoming payload actually is before you start rewriting keys, paste it into the JSON Decoder first.
Which convention each ecosystem expects
| Layer | Convention | Notes |
|---|---|---|
| Python, Ruby | snake_case |
PEP 8 names functions and variables in lowercase with underscores |
| JavaScript, Java | lowerCamelCase |
Google's Java style uses it for methods, fields and parameters |
| SQL columns | snake_case |
Survives both upper- and lower-case identifier folding |
| Protobuf JSON | lowerCamelCase |
Converted from the proto field name automatically |
Protobuf is the interesting case because it makes the mapping normative rather than stylistic. Per the ProtoJSON specification, message field names are mapped to lowerCamelCase for use as JSON object keys, the json_name field option overrides that per field, and conformant parsers must accept both the camelCase form and the original proto name. One canonical spelling, one explicit escape hatch, and readers that tolerate either — that is the pattern worth copying.
Why does snake_case to camelCase break acronyms?
Because the information is already gone. http_status is a five-letter word, an underscore and a six-letter word. It might have been written as HTTPStatus, HttpStatus or httpStatus before someone snake-cased it, and the string retains no evidence of which. A converter cannot restore what was never stored; it can only apply a rule.
The Google rule: lowercase everything, then capitalise once
The Google Java Style Guide addresses this by refusing to special-case acronyms at all. Its §5.3 scheme splits the prose form into words, lowercases everything including acronyms, then uppercases only the first character of each word. The examples it gives are unambiguous: "XML HTTP request" becomes XmlHttpRequest, not XMLHTTPRequest; "new customer ID" becomes newCustomerId, not newCustomerID; "supports IPv6 on iOS?" becomes supportsIpv6OnIos.
This is the rule that makes conversion tractable. If HTTPStatus never gets written in the first place, then http_status → httpStatus is a faithful round-trip rather than a lossy guess. The style guide is choosing predictability over typographic fidelity, and for machine-converted identifiers that is the right trade.
Rails registers acronyms instead of guessing
Rails takes the opposite approach: keep the acronyms, but declare them. ActiveSupport::Inflector::Inflections exposes an acronym method that seeds a lookup table, so camelize can produce spellings a regex would never reach:
acronym 'HTTP'
camelize 'my_http_delimited' # => 'MyHTTPDelimited'
The documentation is candid about the limits. An acronym is only recognised as a delimited unit, so with HTTP registered, camelize 'https' yields Https, not HTTPs, and underscore 'HTTPS' yields http_s rather than https — you have to register HTTPS separately. The registry works precisely because a human supplies the knowledge the string lost.
What a converter can and cannot recover
Recoverable: word boundaries marked by single underscores between lowercase words. Not recoverable: which of those words was an acronym, what its original capitalisation was, and whether an underscore was a boundary or part of a name. Any tool promising otherwise is guessing from a wordlist, and wordlists are wrong at exactly the moments that matter — internal product names, vendor prefixes, protocol versions.
Digits break more conversions than acronyms do
Acronyms get the attention, but digits cause more real bugs, because converters disagree about whether a digit starts a new word. Uppercase and lowercase are the same thing for 2, so there is no boundary marker to preserve.
What lodash actually returns
Running lodash 4.18.1 over a set of realistic field names, converting to camelCase and then back to snake_case:
| Input | camelCase | Back to snake_case |
|---|---|---|
user_id |
userId |
user_id |
oauth2_token |
oauth2Token |
oauth_2_token |
ipv6_address |
ipv6Address |
ipv_6_address |
is_2fa_enabled |
is2FaEnabled |
is_2_fa_enabled |
The first row is the case everyone tests. The other three are the ones that reach production. is_2fa_enabled is the worst of them: it acquires a capital F mid-word on the way out and an extra underscore on the way back, so the two sides of your API disagree about a field name that looked completely ordinary in the schema.
Underscores that vanish entirely
Three shapes disappear rather than shift:
- Leading underscores.
_privatebecomesprivate— andprivateis a reserved word in several target languages. - Doubled underscores.
foo__barandfoo_barboth becomefooBar, so two distinct keys collide into one. - Trailing underscores.
class_, the standard Python workaround for a keyword clash, becomesclass, reintroducing the exact clash it existed to avoid.
None of these throw. They produce a plausible-looking identifier that quietly refers to something else, which is why they survive code review and fail in integration.
Protobuf documents the same failure
This is not a lodash quirk. The ProtoJSON specification calls out google.protobuf.FieldMask as its most significant round-trip limitation: field names are converted from snake_case to lowerCamelCase for the JSON representation, and converting the camelCase path segments back is not lossless. Its own examples are field names containing underscores followed by numbers — x_0 camelises to x0, which snake-cases back to x0, losing the underscore — and names with consecutive or leading underscores, which the capitalisation rules cannot distinguish from ordinary camelCase.
Ordinary fields escape this because the parser can consult the schema to disambiguate. FieldMask paths are not bound to a message type during parsing, so there is nothing to consult. Protobuf's answer is prohibition rather than cleverness: starting in Edition 2024, protoc rejects these naming patterns outright.
Is snake_case to camelCase conversion reversible?
For plain lowercase words separated by single underscores, yes. For anything else, treat it as a one-way function and design around that.
Pick one canonical side of the boundary
The failure mode is not conversion; it is bidirectional conversion. If the backend stores snake_case and the frontend derives camelCase from it on the way out, every name has exactly one source of truth. If both sides convert in both directions, oauth2_token and oauth_2_token will eventually coexist in the same database, one of them written by a code path nobody remembers.
Convert in exactly one place
Three places are defensible, in rough order of preference:
- Code generation. Derive client types from the schema. Names are computed once, at build time, and reviewed in a diff.
- A serialisation layer. One interceptor on the HTTP client, or one naming strategy configured on the JSON library. Every payload passes through the same function.
- Hand-written mappers. Verbose, but explicit — and the only option that expresses "this field is spelled irregularly" without a registry.
What is not defensible is converting ad hoc at call sites, because the rule then lives in a dozen slightly different regexes.
Verify the rename before you commit it
A bulk rename is a text transformation, so review it as one. Generate the converted file, then diff it against the original and read every changed line — the Diff Checker makes the digit-boundary surprises obvious in a way that scanning the new file alone never does. Pay particular attention to keys that appear in stored data, in URLs, or in anything already published to a client you do not control: those are not renames, they are breaking changes wearing a rename's clothes.
References
- ProtoJSON Format — normative lowerCamelCase field-name mapping, the
json_nameoption, and the FieldMask round-trip limitations. - Google Java Style Guide — §5.3 "Camel case: defined"; source for
XmlHttpRequest,newCustomerIdandsupportsIpv6OnIos. - ActiveSupport::Inflector::Inflections — the
acronymregistry, and its documented limits aroundHttpsandhttp_s. - PEP 8 – Style Guide for Python Code — Python's lowercase-with-underscores naming convention.
- Lodash documentation —
_.camelCaseand_.snakeCase; the round-trip table was produced by running lodash 4.18.1 directly.
Related on iKit
- camelCase to snake_case: how to convert without breaking your code — the same boundary from the other direction, including why Postgres folds unquoted identifiers to lower case.
- Regex capture groups explained:
$1,$&and named references — the substitution syntax behind every conversion pattern in this article. - How to use regex find and replace in VS Code — running a naming-convention rename across a whole repository with a preview step first.
- JSON to CSV: how to flatten nested objects safely — where key names stop being internal, because flattened keys become column headers other people build on.
Related posts
REST API Naming: snake_case Backend, camelCase Frontend (2026)
REST API naming forces a choice: snake_case on the backend, camelCase in the client. Here is where to convert, what the round trip loses, and how to decide.
Why Designers Still Use Lorem Ipsum in 2026 (And When Not)
Lorem ipsum survived sixty years of design tooling for one reason: it fails loudly. Here is what filler text is genuinely good at, and where it costs you.
Lorem Ipsum for i18n Testing: Catch Text Overflow (2026)
Lorem Ipsum passes every layout test, then German breaks it. Here is why Latin filler hides i18n bugs and what to paste instead before you localise.