Rename Every JSON Key to camelCase: 3 Patterns (2026)
Renaming every key in a JSON object breaks on nesting, arrays and collisions. Three patterns — interceptor, codegen, manual — and when each one is correct.
Rename Every JSON Key to camelCase
A payload arrives with created_at, user_id and shipping_address, and your TypeScript interfaces all use camelCase. Renaming one key is trivial. Renaming every key — through arrays, three levels of nesting, and a metadata blob whose keys are user-supplied — is where the one-liner off Stack Overflow quietly drops data. Here are the three patterns that hold up, and the traps each one hides.
TL;DR
JSON.parse's reviver transforms values, not key names — it cannot rename.- Rebuild objects recursively into a fresh target; map arrays element by element.
- Three patterns: HTTP interceptor, schema-driven codegen, one-off manual rewrite.
- Check for collisions first:
user_idanduserIdboth becomeuserId. - Convert once at a boundary, never scattered across components or models.
How to rename all keys in a JSON object to camelCase
The mechanics matter more than the naming rule, because the naming rule is the easy part. A conversion that handles user_name correctly still corrupts your payload if it flattens arrays or swallows null.
Why JSON.parse's reviver cannot rename keys
Almost everyone reaches for the reviver first. It looks like exactly the right hook: a callback that sees every key on the way in. It is not.
The reviver receives a key and a value, and — as MDN's JSON.parse() reference documents — the value it returns replaces the value, not the property name. Return undefined and the property is deleted outright.
// Reviver sees "user_name" but cannot
// change the property name.
JSON.parse('{"user_name":"ada"}',
(key, value) => value
);
// -> { user_name: "ada" }
You can reach the containing object through the reviver's this binding and assign a new property there, but you are then fighting the traversal order: MDN specifies that revival is depth-first, deepest properties first, with the root value visited last under an empty-string key. Arrow functions get no this binding at all. It works in demos and breaks on real payloads.
One reviver feature is worth knowing for a different reason. The optional third context argument exposes context.source, the original JSON text of a primitive, which is the supported way to recover a large integer as a BigInt before Number has already rounded it. Renaming keys and preserving numeric precision are separate problems; do not try to solve both in one pass.
How to convert nested JSON keys recursively in JavaScript
The pattern that actually works is a rebuild. Never mutate — construct a new value of the same shape:
const toCamel = (s) =>
s.replace(/_+([a-z0-9])/g,
(_, c) => c.toUpperCase());
function renameKeys(input, fn) {
if (Array.isArray(input)) {
return input.map((v) => renameKeys(v, fn));
}
if (input === null ||
typeof input !== "object") {
return input;
}
if (input instanceof Date) return input;
const out = {};
for (const [k, v] of Object.entries(input)) {
out[fn(k)] = renameKeys(v, fn);
}
return out;
}
Four guards, in order, and each one exists because omitting it corrupts something:
- Array first.
typeof [] === "object", so an array checked second becomes an object with"0","1"keys. nullbeforetypeof.typeof null === "object"is the oldest trap in JavaScript.- Primitives pass through. Strings, numbers and booleans are returned as-is, not iterated.
- Class instances bail out.
Date,Map,RegExpand friends have no enumerable own keys worth rewriting; rebuilding them produces{}.
If you are only handling freshly parsed JSON, the Date guard is unnecessary — JSON.parse never produces one. Keep it if the same helper also runs over in-memory objects.
What __proto__ does to a naive rebuild
out[fn(k)] = value is a plain assignment, and a plain assignment to __proto__ sets the object's prototype instead of creating a property. MDN calls out __proto__ as the single key where JSON text and the equivalent JavaScript object literal diverge. If any part of your payload has user-controlled key names — a metadata bag, a form-builder response, a tags object — build the target with Object.create(null), or write properties through Object.defineProperty, so a hostile key cannot reach the prototype chain.
Pattern 1: rename JSON keys in an HTTP interceptor
This is the retrofit. You have a client already talking to a snake_case API, and you want camelCase everywhere above the network layer without touching a hundred call sites.
Where the interceptor sits in the chain
Axios exposes two hooks, and the axios interceptors documentation describes them as middleware around the request. You convert outbound on the request side and inbound on the response side:
api.interceptors.response.use((response) => {
response.data =
renameKeys(response.data, toCamel);
return response;
});
api.interceptors.request.use((config) => {
if (config.data) {
config.data =
renameKeys(config.data, toSnake);
}
return config;
});
Execution order is worth reading the docs for: axios runs request interceptors last-in-first-out and response interceptors first-in-first-out. If a logging interceptor and a renaming interceptor both touch the payload, the one you registered second sees the request before the first one does. Register the rename closest to the wire.
The cost you pay on every response
An interceptor walks the entire payload on every single request. For a 40-key object that is free. For a 4 MB list endpoint it is a full deep clone plus a string transform per key, on the main thread, before your component ever renders.
It is also structurally blind. The interceptor has no idea that metadata holds user keys that must not be touched, or that one legacy endpoint returns PascalCase. Every exception becomes a path check inside a generic function, and that function grows for the life of the project.
When an interceptor is the right call
Use it when the API is consistent, the payloads are small, and you do not control the backend. That combination is common and the pattern is genuinely the cheapest fix available. Do not use it as a permanent architecture on a codebase where you own both ends.
Pattern 2: let the serializer own the mapping
The better answer is to stop transforming at runtime and declare the mapping where the schema already lives.
Jackson, Go struct tags, and declarative naming
Jackson has done this since long before anyone wrote a client-side interceptor. Annotate a class and every property is translated on the way out:
@JsonNaming(
PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Order {
private String customerId; // customer_id
private Instant placedAt; // placed_at
}
The rules are more careful than the regex most people write. Per the Jackson SnakeCaseStrategy javadoc, contiguous capitals are treated as one acronym, so theWWW becomes the_www rather than the_w_w_w; a leading capital gets no underscore, so Results becomes results; and a capital already preceded by an underscore is not given a second one, so user_Name becomes user_name. Those three exceptions are exactly the cases a naive replace(/([A-Z])/g, "_$1") gets wrong.
Go takes the same idea further down: the field tag is the wire name, checked at compile time, visible in the struct definition. There is no ambiguity about what CustomerID serialises to, because you wrote it.
Generating typed models from a committed schema
If you have a JSON Schema or an OpenAPI document, generate the models. quicktype produces types and converters from JSON, JSON Schema, TypeScript or GraphQL queries across roughly twenty target languages, and its own README recommends the durable workflow: infer a schema from a sample, review and edit it, commit the schema to the repo, then generate model code as a build step.
The consequence is that iOS, Android and Node models are all generated from one artifact and therefore agree by construction. Nothing renames anything at runtime.
Why declarative mappings survive refactors
A rename rule expressed as a regex is invisible to your tooling. A rename rule expressed as an annotation, a struct tag or a generated model is a symbol: rename the field in your IDE and the mapping follows. When a field is deleted, the generator fails loudly instead of silently producing undefined.
| Pattern | Runs at | Best when |
|---|---|---|
| Interceptor | Every request | You don't own the API |
| Serializer / codegen | Build or serialize time | You own both ends |
| Manual one-off | Once, by hand | Fixtures, seeds, migrations |
Pattern 3: rename the keys once, by hand
Sometimes the JSON is not a stream. It is a fixture file, a seed dataset, or an export you have to load once and never think about again. Running a build pipeline for that is theatre.
An explicit key map beats a clever regex
For a one-off, write the mapping out. Twenty lines of { "old": "new" } is auditable in a code review; a regex with three lookarounds is not:
const MAP = {
cust_no: "customerNumber",
dob: "dateOfBirth",
addr_1: "addressLine1",
};
const rename = (k) => MAP[k] ?? toCamel(k);
Explicit entries also fix the things automatic conversion cannot know. No case converter turns dob into dateOfBirth or decides that addr_1 should keep its digit. For interactive work on a single file, paste the key list into iKit's Case Converter to generate the right-hand column, then hand-correct the abbreviations.
How to handle key collisions when converting to camelCase
Case conversion is not injective. user_id, userId and user__id all converge on userId, and a blind rebuild silently keeps whichever came last in iteration order. Detect it instead:
function assertNoCollisions(obj, fn) {
const seen = new Map();
for (const k of Object.keys(obj)) {
const next = fn(k);
if (seen.has(next)) {
throw new Error(
`collision: ${seen.get(next)} + ${k}`
);
}
seen.set(next, k);
}
}
The related failure is round-tripping. Converting back is lossy whenever an underscore is not followed by a letter:
| Original | camelCase | Back to snake |
|---|---|---|
custom_label_0 |
customLabel0 |
custom_label0 |
_internal |
internal |
internal |
user__id |
userId |
user_id |
Treat the conversion as one-directional unless you have tested the round-trip on your actual key set.
Verifying the rewrite before you ship it
Two checks cost a minute and catch almost everything. Count the leaf values before and after — a correct rename changes names, never the number of values. Then diff the two files side by side in iKit's Diff Checker; every changed line should be a key, and any changed value is a bug you just caught. Pretty-print both sides first with the JSON Decoder so the diff compares structure rather than formatting. If the data started life as a spreadsheet export, the CSV ↔ JSON Converter will give you a clean starting object before any renaming happens.
All four run entirely in your browser — no upload, which matters when the fixture you are renaming contains real customer records.
Which pattern should you actually pick?
Work backwards from who owns the schema:
- You own both ends: codegen from a committed schema. Everything else is a workaround.
- You own only the client: an interceptor, scoped to one axios instance, not the global default.
- It is a file, not a feed: an explicit key map, verified with a diff, done.
- Keys are user-supplied: do not rename them at all. Treat that subtree as opaque data.
The mistake that costs the most is not picking the wrong pattern — it is picking two. A codebase with an interceptor and per-component snake_case fallbacks has no single answer to "what is this field called", and every new developer adds a third convention while trying to work it out.
References
- JSON.parse() - JavaScript | MDN — reviver semantics: value replacement, deletion on
undefined, depth-first traversal, thecontext.sourceparameter, and the__proto__divergence. - PropertyNamingStrategies.SnakeCaseStrategy (jackson-databind API) — the documented translation rules, including contiguous-acronym, leading-capital and existing-underscore exceptions.
- Interceptors | axios — request/response interceptor API and the LIFO/FIFO execution order used in the interceptor section.
- GitHub - glideapps/quicktype: Generate types and converters from JSON, Schema, and GraphQL — supported inputs and target languages, and the recommended commit-the-schema-then-generate workflow.
- encoding/json package documentation — struct tag semantics cited for compile-time-visible wire names.
Related on iKit
- Converting camelCase to snake_case without breaking your code — the write direction of the rename, and the regex whose acronym handling this article's
toCameldeliberately keeps simple. - snake_case to camelCase: the boundary rule for acronyms — the read direction, and why
user_uuidanduser_idneed different treatment. - REST API naming: snake_case backend, camelCase frontend — the architectural version of this question: which layer should own the conversion at all.
- camelCase vs snake_case vs kebab-case: which convention goes where — the map of which format enforces which convention, useful before you pick a target casing.
- Why XMLHttpRequest becomes xml_http_request — the acronym boundary rule that Jackson's contiguous-capitals exception implements.
- PascalCase for class and type names — the convention your generated model classes will use, whichever pattern you choose.
- CONSTANT_CASE for environment variables — the one target casing where conversion is constrained by POSIX rather than by style.
Related posts
15 Best Free Online Developer Tools to Bookmark (2026)
The 15 free online developer tools worth a permanent bookmark in 2026 — JSON, JWT, diff, hashing, images and colour, all running in your browser.
CONSTANT_CASE for Environment Variables: A 2026 Guide
CONSTANT_CASE is the only naming convention environment variables reliably survive. Here is what POSIX requires, what breaks, and how to convert safely.
How Many Words of Lorem Ipsum Does a Mockup Need? (2026)
How many words of lorem ipsum a mockup needs depends on the container, not habit. Here is the formula, per-block targets, and why 30 words is everywhere.