Skip to content
APIs

JSON Key Naming Conventions: camelCase vs snake_case for APIs

·7 min read
JSON Key Naming Conventions: camelCase vs snake_case for APIs - APIs guide from JSON Prettifier

Quick answer: JSON itself has no naming rule; any string is a valid key. For an API, use camelCase if your main consumers are JavaScript or TypeScript (it is what the Google JSON Style Guide recommends) and snake_case if they are Python or Ruby (it is what GitHub and Stripe use). What matters far more than the case style is being consistent across every endpoint, starting keys with a letter, avoiding $, @, spaces and hyphens, and naming arrays in the plural.

The v1 endpoint returns user_id. The v2 endpoint, written by a different team, returns userId. The webhook payload, generated from a database row, returns UserID. None of these is wrong on its own. Together they mean every client has three mapping layers, three sets of tests, and a bug every time someone copies a field name from the wrong doc. Naming conventions exist to prevent that specific afternoon.

Use JSON Compare to inspect added, removed, and changed values in two non-sensitive sample documents.

What the Spec Says (Almost Nothing)

RFC 8259 defines an object member as a string followed by a colon and a value. That is it. The key can be "first name", "2fa", "$set", or an emoji. The spec also says keys should be unique, and leaves order unspecified. Every rule below comes from the languages and tools that read JSON, not from JSON.

camelCase vs snake_case: Who Uses What

The honest answer is that both styles are mainstream and both have major APIs behind them. As of their public documentation:

StyleExampleUsed by
camelCasecreatedAt, userIdGoogle JSON Style Guide, Microsoft REST guidelines, Kubernetes, most JavaScript APIs
snake_casecreated_at, user_idGitHub REST, Stripe, Twitter/X, Slack, most Python and Ruby APIs
PascalCaseCreatedAt, UserIdMany AWS service APIs, older .NET APIs
kebab-casecreated-atRare in JSON; needs bracket access in JavaScript

The pattern is not random. APIs that grew up next to JavaScript front ends (Google's, Microsoft Graph, Kubernetes) lean camelCase because the keys become identifiers in client code without a mapping step. APIs built in Ruby or Python shops (GitHub, Stripe, Twitter) lean snake_case for the same reason in the other direction. AWS is the outlier: many of its service APIs use PascalCase (TableName, FunctionName), with variation between services.

So the decision rule is simple. Who will read these keys most often? Match them. If the answer is "both, equally", choose camelCase: it is the style that needs no conversion in the browser, and Python consumers can live with data["createdAt"] more easily than a React app can live with data.created_at under a linter that enforces camelCase identifiers.

The Rules That Matter More Than the Case Style

RuleWhy
Be consistentOne style per API, every endpoint, every version
Letters, digits, underscore onlyNo spaces, hyphens, or dots; they break dot access and many tools
Start with a letterA leading digit or $ is legal JSON but hostile to most languages
Avoid $, @, and __ prefixes$ clashes with MongoDB operators, @ with JSON-LD, __proto__ with JavaScript
Plural names for arraysitems, not item or itemList
is / has / can for booleansisActive, hasChildren, canEdit
Suffix units and formatstimeoutMs, sizeBytes, createdAt (timestamp)
No abbreviations unless universalid and url are fine; usr and crtd are not

A payload that follows all of them looks like this:

{
  "id": "ord_8f3a",
  "createdAt": "2026-05-06T10:15:00Z",
  "isPaid": true,
  "hasShipped": false,
  "totalCents": 4999,
  "currency": "USD",
  "items": [
    { "sku": "A-100", "quantity": 2, "unitPriceCents": 1999 }
  ],
  "shippingAddress": null
}

Notice totalCents rather than total, and createdAt rather than created. The suffix tells the reader the unit and the type without opening the docs. (The reason money is in cents is covered in JSON number precision.) And shippingAddress: null rather than a missing key is a deliberate choice; the null vs missing post explains when each is right.

Why the dollar sign and __proto__ are dangerous, not just ugly

Most naming rules are about convenience. Two are about safety. MongoDB treats keys beginning with $ as operators, so a user-supplied document containing "$where" can change the meaning of a query. And in JavaScript, parsing {"__proto__": {"isAdmin": true}} and merging it into another object with a naive recursive merge can pollute Object.prototype. JSON.parse itself is safe (it creates an own property named __proto__), but the merge step afterward is not. Reject or rename such keys at the boundary; the JSON security best practices guide shows the full pattern.

Converting Between Conventions

You will not always control both ends. The database is snake_case, the API is camelCase, the partner feed is PascalCase. The right place for the conversion is the serialization layer, configured once, so no handler ever spells a key by hand.

Java (Jackson)

// Per class
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Order {
    private String createdAt;   // serialized as "created_at"
    private boolean isPaid;     // serialized as "is_paid"
}

// Or globally
ObjectMapper mapper = new ObjectMapper()
    .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);

Rust (serde)

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Order {
    created_at: String,   // "createdAt" on the wire
    is_paid: bool,        // "isPaid"
    #[serde(rename = "SKU")]
    sku: String,          // one-off override
}

Python (pydantic)

from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel

class Order(BaseModel):
    model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
    created_at: str
    is_paid: bool

Order(created_at="2026-05-06T10:15:00Z", is_paid=True).model_dump(by_alias=True)
# {'createdAt': '2026-05-06T10:15:00Z', 'isPaid': True}

Without pydantic, a ten-line recursive function does the job for dicts coming out of the json module:

import re

def to_snake(key):
    return re.sub(r"(?<!^)(?=[A-Z])", "_", key).lower()

def snake_keys(value):
    if isinstance(value, dict):
        return {to_snake(k): snake_keys(v) for k, v in value.items()}
    if isinstance(value, list):
        return [snake_keys(v) for v in value]
    return value

JavaScript / TypeScript

const toCamel = (s) => s.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());

function camelKeys(value) {
  if (Array.isArray(value)) return value.map(camelKeys);
  if (value !== null && typeof value === "object" && value.constructor === Object) {
    return Object.fromEntries(
      Object.entries(value).map(([k, v]) => [toCamel(k), camelKeys(v)])
    );
  }
  return value;
}

camelKeys({ created_at: "2026-05-06", line_items: [{ unit_price: 5 }] });
// { createdAt: "2026-05-06", lineItems: [{ unitPrice: 5 }] }

The value.constructor === Object check keeps Dates, Maps, and class instances out of the rewrite. Packages such as camelcase-keys and snakecase-keys handle the same thing with a deep option and a list of keys to exclude. Go takes a different route: struct tags such as json:"createdAt" name each field explicitly, which is verbose but leaves nothing to guess.

Dynamic Keys: The One Place the Convention Does Not Apply

Naming rules are for fixed keys, the ones a client types into code. An object used as a map, where the keys are data (user IDs, ISO country codes, SKUs), is different. Those keys keep whatever form the data has, and a converter must leave them alone:

{
  "pricesByCurrency": { "USD": 1999, "EUR": 1849 },
  "usersById": { "u_8f3a": { "displayName": "Priya" } }
}

Two things follow. First, name the containing field so the reader knows it is a map: pricesByCurrency, usersById. Second, exclude those branches from any recursive case converter, or u_8f3a becomes u8f3a and every lookup fails. Most converter packages take a list of paths to skip for exactly this reason. If a map's keys are unbounded and the client only ever iterates, consider an array of objects with an explicit id field instead; it serializes predictably and never collides with a naming rule.

Reviewing an API for Naming Drift

Conventions decay one pull request at a time. Three cheap checks keep them honest:

  1. Schema-level enforcement. Add propertyNames with a pattern to your JSON Schema, for example {"propertyNames": {"pattern": "^[a-z][a-zA-Z0-9]*$"}} for camelCase, and run it in CI against example responses.
  2. Sort and skim. Paste a real response into JSON Prettifier, turn on Sort Keys, and read the key column top to bottom. A stray user_id between updatedAt and userName jumps out immediately when the keys are alphabetized.
  3. Diff old against new. When a field is renamed, keep the old name for one deprecation window and compare the two payloads so the change is visible in review rather than discovered by a client.

Common Mistakes

  • Leaking database casing. A column named CustomerID becomes a JSON key named CustomerID because nobody configured the ORM. Set the mapping once (Prisma's @map, SQLAlchemy's key=, Jackson's naming strategy).
  • Acronyms. userID, userId, and user_i_d all appear in the wild from careless converters. Treat acronyms as words: userId, htmlUrl, user_id, html_url.
  • Type in the name. nameString, countInt, and itemsArray add noise; the value already has a type. Units (Ms, Bytes) are the exception because they are not visible from the value.
  • Negative booleans. isNotVerified: false requires a double negative to read. Prefer isVerified: true.
  • Renaming without a version. Changing created to createdAt in place is a breaking change for every consumer. The JSON in REST APIs guide covers versioning strategies.

Sending those keys to an XML API introduces another boundary: sanitization can make distinct names collide. Run these five JSON-to-XML conversion checks before assuming readable output preserves field identity and receiver requirements.

Frequently Asked Questions

Should JSON keys be camelCase or snake_case?
Either works; consistency matters more than the choice. camelCase matches JavaScript and TypeScript and is recommended by the Google JSON Style Guide. snake_case matches Python and Ruby and is used by GitHub and Stripe. Pick the one your primary consumers will type most, and apply it everywhere.
What naming convention does the JSON spec require?
None. RFC 8259 only says a key is a string, so spaces, emoji, and a leading digit are all legal. The conventions come from the languages that read the JSON, which is why keys that work as identifiers in most languages (letters, digits, underscore, starting with a letter) are the safe choice.
Can JSON keys contain spaces or hyphens?
Yes, but you will regret it. A key like "first name" or "first-name" forces bracket access in JavaScript (data["first-name"]), cannot map to a struct field in Go or Rust without renaming, and confuses many query tools. Use firstName or first_name instead.
How do I convert snake_case JSON to camelCase in JavaScript?
Write a small recursive function that rewrites each key with str.replace(/_([a-z])/g, (m, c) => c.toUpperCase()), or use a package such as camelcase-keys with the deep option. Do the conversion once at the API boundary rather than scattered through components.
Why should JSON keys not start with a dollar sign?
In MongoDB, keys that start with $ are operators, so a document with such a key can be rejected or, worse, interpreted as a query. Some templating and configuration tools also treat $ specially. The same caution applies to @ (reserved by JSON-LD) and __proto__ (a prototype-pollution vector in JavaScript).

Spot Naming Drift in One Scroll

Paste a response, turn on Sort Keys, and read the alphabetized key list. Mixed conventions stand out instantly. Nothing leaves your browser.

Sort My JSON Keys