XMLHttpRequest to xml_http_request: The Acronym Rule (2026)
XMLHttpRequest becomes xml_http_request only if your converter knows where the acronym ends. Here is the boundary rule, the regex, and what style guides say.
XMLHttpRequest to xml_http_request: The Acronym Rule
Run XMLHttpRequest through a one-line camelCase-to-snake_case regex and you get x_m_l_http_request. Run it through a slightly better one and you get xmlhttp_request. Neither is what you wanted. Acronym runs have no lower-to-upper transition to split on, so the boundary is invisible to the usual pattern — and every major style guide resolves the ambiguity differently. Here is the rule that actually produces xml_http_request.
TL;DR
- A single lower-to-upper boundary is not enough; acronym runs need a second rule.
- Split a capital run one character early when a lowercase letter follows it.
XMLHttpRequest→XML+Http+Request→xml_http_request.- Style guides disagree: Java lowercases acronyms, PEP 8 and Go do not.
- snake_case is lossy — you cannot reconstruct
XMLHttpRequestwithout a dictionary.
Why XMLHttpRequest breaks camelCase to snake_case converters
The name itself is the joke. The WHATWG XMLHttpRequest Standard defines the interface as XMLHttpRequest — XML shouted, Http politely PascalCased, in a single identifier. It is the canonical test case because it contains both boundary types a converter has to recognise, and it is the example Google's Java style guide reaches for too.
The naive regex and what it does
Almost every snippet you will find looks like this:
// Naive: one underscore per capital
const snake = (s) =>
s.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase())
.replace(/^_/, "");
snake("userName"); // user_name ✅
snake("XMLHttpRequest"); // x_m_l_http_request ❌
It works on userName and falls apart on anything with an initialism, because it treats every capital as a word start. The usual fix is to require a lowercase character on the left:
const snake = (s) =>
s.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.toLowerCase();
snake("totalItemCount"); // total_item_count ✅
snake("XMLHttpRequest"); // xmlhttp_request ❌
Better, and still wrong. There is no lowercase letter between L and H, so the regex sees XMLH as one unbroken run and only splits at p → R.
Where the word boundary actually is
Look at the capitals in sequence: X M L H. Three of them belong to XML; the fourth starts Http. The signal is not in the capitals at all — it is in the lowercase t that follows H. A capital letter followed by a lowercase letter is the first letter of a new word. Every capital before it belongs to the run that came before.
That gives you the second boundary rule: split between a capital and the capital that precedes it, when the second capital is followed by a lowercase letter.
The two-boundary rule, stated once
| Boundary | Pattern | Example |
|---|---|---|
| Lower → upper | aB |
item | Count |
| Upper → upper + lower | ABc |
XML | Http |
| Letter → digit | a1 |
sha | 256 |
Apply both letter rules and XMLHttpRequest tokenises as XML, Http, Request, which lowercases and joins to xml_http_request. If you would rather not hand-roll it, iKit's Case Converter applies both boundaries and runs entirely in your browser tab.
How to convert camelCase with acronyms to snake_case in JavaScript
A two-pass regex that handles acronym runs
Two replacements, in this order:
function toSnake(s) {
return s
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.toLowerCase();
}
toSnake("XMLHttpRequest"); // xml_http_request
toSnake("parseJSONBody"); // parse_json_body
toSnake("getHTTPStatus"); // get_http_status
toSnake("userID"); // user_id
The first pass handles acronym-then-word. The second handles word-then-anything. Order matters: run them the other way round and the first pass has already consumed the boundary the second one needs.
Digits are a third boundary class
Digits have no case, so neither letter rule fires on them. sha256Hash becomes sha256_hash, not sha_256_hash — usually correct, occasionally not. If your codebase writes base64Encode and expects base_64_encode, you need an explicit digit rule, and you will then have to special-case utf8, oauth2, and every version number in the project. Most teams leave digits attached and move on.
Test the pattern before you ship it
A regex that rewrites identifiers across a codebase deserves five minutes in a tester with real inputs. Paste the two patterns and a list of your worst names — ID, iOS, gRPCClient, parseHTMLToJSON — into iKit's Regex Tester and check the match groups before you point the script at anything. When you do run it, diff the before and after rather than trusting the output, because the failures are quiet: a wrong underscore compiles fine and breaks at runtime.
Should acronyms be uppercase in camelCase and PascalCase?
There is no universal answer, which is exactly why converters disagree. Four major style guides, four different results for the same English phrase.
Google's Java style: lowercase everything, then capitalise
The Google Java Style Guide gives a deterministic algorithm: convert the phrase to plain ASCII, split into words, lowercase everything including acronyms, then uppercase the first character of each word. The prose form "XML HTTP request" is listed with XmlHttpRequest as correct and XMLHTTPRequest as incorrect. The guide is explicit that the casing of the original words is almost entirely disregarded — which is what makes the scheme predictable.
PEP 8, Go, and .NET: keep the acronym intact
Python goes the other way. PEP 8's note on CapWords says to capitalise all the letters of an acronym, preferring HTTPServerError to HttpServerError.
Go is stricter still. The Go style guide's initialisms section requires every letter within a given initialism to share a case, so an exported "XML API" is XMLAPI and the unexported form is xmlAPI. XmlApi and xmlApi are both listed as incorrect.
.NET splits the difference by length. The Framework Design Guidelines PascalCase acronyms over two letters — hence HtmlTag — but make a special case for two-letter acronyms, which keep both capitals, as in IOStream. The camelCased parameter form lowercases both: ioStream.
Rust: an acronym counts as one word
The Rust API Guidelines state that in UpperCamelCase, acronyms and contractions count as a single word: Uuid rather than UUID, Stdin rather than StdIn. In snake_case they are simply lowercased.
| Ecosystem | "XML HTTP request" | Acronym rule |
|---|---|---|
| Google Java | XmlHttpRequest |
Lowercase, then capitalise first letter |
| PEP 8 | XMLHTTPRequest |
Capitalise all letters of the acronym |
| Go (exported) | XMLHTTPRequest |
Uniform case within each initialism |
| .NET | XmlHttpRequest |
PascalCase acronyms over two letters |
| Rust | XmlHttpRequest |
Acronym counts as one word |
The practical takeaway: pick the rule your linter enforces and stop arguing. golangci-lint will fail your XmlApi; ruff and pylint will not care; Google's google-java-format has no opinion on names at all, so the Java rule is social rather than mechanical.
How to handle iOS, gRPC, and other mixed-case initialisms
Why case-only heuristics cannot see these
iOS starts lowercase. gRPC starts lowercase. DDoS has a lowercase letter in the middle. No boundary rule based purely on case transitions can recover the intended word — supportsIPv6OnIOS will tokenise into something no human would choose, and the Google Java guide's answer is simply to give up on the original casing and produce supportsIpv6OnIos.
Go takes the opposite position and enumerates the cases: exported iOS becomes IOS, unexported stays iOS; exported gRPC becomes GRPC, unexported stays gRPC; DDoS keeps its internal lowercase when exported and flattens to ddos when not.
The dictionary escape hatch
If you need iOS to survive a conversion, no algorithm will do it — you need a list. Practical converters keep a small set of known initialisms and check it before applying the case rules:
const KNOWN = new Set([
"ios", "grpc", "ddos", "oauth", "graphql",
]);
function tokens(s) {
return s
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.toLowerCase()
.split(" ");
}
// tokens("gRPCClient") -> ["g", "rpc", "client"]
// ...then re-join "g"+"rpc" via the KNOWN set
Keep the list short and project-specific. A dictionary that tries to cover every initialism in computing will mis-fire on somebody's variable named id or os sooner than you think.
Why xml_http_request cannot become XMLHttpRequest again
The information the underscore throws away
snake_case records word boundaries and nothing else. xml_http_request tells you there are three words; it does not tell you that two of them were shouted. Reverse it with the standard rule — uppercase the letter after each underscore — and you get XmlHttpRequest, which is correct Java, correct .NET, correct Rust, and wrong for the actual DOM interface.
This is why round-tripping is the wrong mental model. The conversion is a projection, not a bijection.
| Original | snake_case | Naive reverse |
|---|---|---|
XMLHttpRequest |
xml_http_request |
XmlHttpRequest |
userID |
user_id |
UserId |
parseJSONBody |
parse_json_body |
ParseJsonBody |
iOSVersion |
i_os_version |
IOsVersion |
What this means at the API boundary
If your backend serialises snake_case JSON and your frontend expects camelCase, run the mapping in exactly one direction and treat one side as authoritative. Converting on both ends, independently, is how a field named userID turns into userId on the way out and user_id on the way back — until somebody adds userIDList and the two halves disagree. When you are inspecting a payload to see which convention actually arrived on the wire, convert the response to a table and read the header row rather than guessing from the code.
Rename once, in one place
The safest pattern is a single serialisation layer that owns the transformation, plus an explicit override map for the handful of names the algorithm gets wrong. Ten entries covering id, url, xml, and your three worst product acronyms will handle more real cases than any clever regex.
References
- Google Java Style Guide — Camel case: defined — the deterministic lowercase-everything scheme and the "XML HTTP request" →
XmlHttpRequestexample. - Go Style Decisions — Initialisms — the uniform-case-within-an-initialism rule and the exported/unexported table for
iOS,gRPC, andDDoS. - Capitalization Conventions - Framework Design Guidelines — the .NET rule for acronyms over two letters and the two-letter exception (
IOStream). - PEP 8 – Style Guide for Python Code — the CapWords note preferring
HTTPServerErroroverHttpServerError. - Rust API Guidelines — Naming — acronyms count as one word in
UpperCamelCase(Uuid,Stdin). - XMLHttpRequest Standard — confirmation of the interface's exact spelling.
Related on iKit
- Converting camelCase to snake_case without breaking your code — the general conversion this article's acronym rule plugs into, including the round-trip losses.
- Going the other way: snake_case to camelCase, acronyms intact — the reverse direction, where the missing acronym information bites hardest.
- PascalCase and the acronym rules that trip up class names — the same style-guide disagreement, framed around type and class naming.
- camelCase vs snake_case vs kebab-case: which convention goes where — the decision table for picking a convention before you worry about acronyms inside it.
- Why hyphens win in CSS, URLs, and CLI flags — the one convention where case never matters, and why that makes acronyms a non-problem.
Related posts
kebab-case in CSS, URLs, and CLI Flags: Why Hyphens Win (2026)
kebab-case shows up in CSS properties, URL slugs, and command-line flags for reasons that are not stylistic. Here is what the specs actually require.
How a Lorem Ipsum Generator Works: The Word Bank (2026)
Every lorem ipsum generator is a small word bank plus a few rules. Here is the actual algorithm: 182 Latin words, sentence windows, commas, punctuation.
Where Does Lorem Ipsum Come From? The Real Story (2026)
Where does Lorem Ipsum come from? Not an unknown printer in the 1500s. The paper trail runs from Cicero in 45 BC to a 1914 Loeb edition to Letraset in 1966.