JSON Key Naming Conventions: camelCase vs snake_case vs kebab-case (and PascalCase)
Last updated:
JSON itself is silent on key naming. RFC 8259 says a key is any string and stops there — no case rule, no character restriction beyond what a JSON string allows. The convention you ship is a product decision shaped by your consumers, your language ecosystem, and the APIs you want to look like. Four styles dominate in practice: camelCase (JavaScript, Microsoft Graph), snake_case (Python, Ruby, GitHub, Stripe, Twitter v2, most of Google Cloud), kebab-case (rare in JSON bodies, common in URL paths and CSS), and PascalCase (.NET, AWS IAM, CloudFormation). This guide is a reference for what each style looks like, what real APIs ship today, how the four interact with language ergonomics, and a decision matrix for picking one for your own API.
Got a JSON payload with mixed key styles and want to spot the inconsistency at a glance? Paste it into Jsonic's JSON Validator — the structured view makes case mismatches and stray casing jump out immediately.
Open JSON ValidatorThe four conventions: camelCase, snake_case, kebab-case, PascalCase
Every JSON key naming convention is a rule for how to combine multi-word names into a single identifier. The four styles you see in practice differ in two dimensions: the separator (none, underscore, or hyphen) and the capitalization of the first letter.
// Same data, four conventions
// camelCase — JavaScript / Microsoft Graph style
{
"userId": 1,
"firstName": "Ada",
"createdAt": "2026-05-23T09:00:00Z",
"isVerified": true
}
// snake_case — Python / GitHub / Stripe style
{
"user_id": 1,
"first_name": "Ada",
"created_at": "2026-05-23T09:00:00Z",
"is_verified": true
}
// kebab-case — legal JSON, awkward in most languages
{
"user-id": 1,
"first-name": "Ada",
"created-at": "2026-05-23T09:00:00Z",
"is-verified": true
}
// PascalCase — .NET / AWS IAM heritage
{
"UserId": 1,
"FirstName": "Ada",
"CreatedAt": "2026-05-23T09:00:00Z",
"IsVerified": true
}Each convention has secondary rules that matter for consistency. How do you handle acronyms — userURL or userUrl in camelCase? How do you handle numbers — v2Endpoint or v_2_endpoint? How do you treat leading capitals after a separator — API_KEY or api_key in snake_case? These sub-decisions matter as much as the top-level choice; document them in your style guide.
A fifth style — SCREAMING_SNAKE_CASE — exists for constants in many languages but almost never appears in JSON keys. It shows up in environment variable names and JSON-Schema enum values, not in object keys.
What major APIs actually ship
Style guides describe intent; production APIs describe what shipped. The two do not always match. Here is what you see on the wire from the largest public APIs as of 2026.
| API | Convention | Notes |
|---|---|---|
| GitHub REST API | snake_case | Consistent across v3 and the newer fine-grained-token endpoints |
| Stripe API | snake_case | Hard rule for over a decade; matches Ruby heritage |
| Twitter v2 API | snake_case | Changed from v1.1 (which mixed camelCase) — v2 is uniformly snake |
| Slack API | snake_case | Standard across Web API, Events API, RTM |
| Shopify Admin API (REST) | snake_case | GraphQL API uses camelCase — same product, two conventions |
| Google Cloud REST APIs | snake_case | Protobuf-generated default; opt-in camelCase mapping exists per service |
| Google Maps / YouTube Data API | camelCase | Follows the Google JSON Style Guide |
| Microsoft Graph | camelCase | Uniform across mail, calendar, Teams, OneDrive |
| Vercel REST API | camelCase | TypeScript-first ecosystem; matches the SDK shape |
| Auth0 Management API | snake_case (mostly) | Some camelCase fields on newer endpoints |
| AWS IAM / CloudFormation | PascalCase | Locked in by the original SDK design; unchangeable |
| AWS DynamoDB / Lambda response | camelCase (mixed) | Newer services moved away from PascalCase |
| OpenAI API | snake_case | Matches the Python-heavy ML ecosystem |
| Anthropic API | snake_case | Same convention as OpenAI for ecosystem consistency |
The pattern across this table: snake_case is the modal choice for new large public APIs, especially in domains where Python clients are common (ML, payments, source control, messaging). camelCase clusters around Microsoft, JS-first platforms, and GraphQL APIs. PascalCase survives only in AWS and .NET heritage corners. kebab-case does not appear in any major API's object keys.
Language conventions vs API conventions
Programming languages each have a property-naming convention baked into their ecosystem. When the JSON convention matches the language convention, code reads naturally. When they disagree, you need a translation layer — explicit renames at every boundary or a generic transformer.
| Language | Property convention | Native JSON convention |
|---|---|---|
| JavaScript / TypeScript | camelCase | camelCase |
| Python | snake_case (PEP 8) | snake_case |
| Ruby | snake_case | snake_case |
| Rust | snake_case (warn on camel) | snake_case with #[serde(rename_all)] escapes |
| Go | PascalCase (exported) / camelCase (internal) | snake_case via struct tags is common |
| Java | camelCase | camelCase |
| C# / .NET | PascalCase | camelCase on the wire (modern), PascalCase on legacy |
| Swift | camelCase | camelCase |
| Kotlin | camelCase | camelCase |
The interesting cases are Go and Rust. Go's ecosystem prefers internal PascalCase for exported fields, but the standard library's encoding/json package supports struct tags that map any wire format to any field name — so Go projects routinely consume snake_case wire formats with PascalCaseGo types. Rust's serde works the same way with #[serde(rename_all = "snake_case")] at the struct level.
The takeaway: pick the convention that matches your dominant consumer language, and use the language's native translation mechanism for everything else. Avoid trying to be language-neutral — there is no such thing, and snake_case is only "neutral" in the sense that it forces friction on JS/TS readers equally to how camelCase forces friction on Python readers.
kebab-case: legal in JSON but requires bracket notation in JS
kebab-case keys (user-id, first-name) are valid JSON. Every conforming parser accepts them. The friction is in how every consuming language reads them after parsing. In JavaScript, the dot operator interprets a hyphen as subtraction, so you cannot say response.user-id — that parses as response.user minus the variable id.
// Legal JSON
const response = { "user-id": 1, "first-name": "Ada" }
// Compile error or wrong semantics
response.user-id // ReferenceError: id is not defined
response.first-name // tries to evaluate response.first - name
// Required workarounds
response["user-id"] // bracket notation works
const { "user-id": userId, // destructure with rename
"first-name": firstName } = response
// In TypeScript, types stay quoted too
type User = {
"user-id": number
"first-name": string
}
const u: User = { "user-id": 1, "first-name": "Ada" }The same friction applies in Python (response["user-id"] required, no attribute access), Ruby, Rust, Go, and every other language with identifier rules that exclude hyphens. The only languages where dotted access to kebab-case keys works are XML/XSLT, CSS, and shell command-line flags — none of which are common JSON consumers.
The historical reason kebab-case appeared as a JSON option is JSON:API's earlier "preferred member names" recommendation, which favored hyphens for consistency with URL paths. That recommendation was softened in later revisions, and almost no implementations followed it for object keys. Reserve kebab-case for the contexts where it makes sense: URL path segments, HTML attributes, CSS properties, and CLI flags. Inside JSON bodies, pick snake_case or camelCase.
Reserved characters and what is actually legal in a JSON key
RFC 8259 defines a JSON key as any valid JSON string. A JSON string can contain any Unicode character except for unescaped control characters (U+0000 through U+001F), unescaped double quotes, and unescaped backslashes. Everything else — letters, digits, spaces, emoji, Chinese characters, dots, hyphens, slashes — is fair game.
// All of these are valid JSON
{
"userId": 1, // ASCII identifier
"user_id": 1, // with underscore
"user-id": 1, // with hyphen
"user id": 1, // with space
"user.id": 1, // with dot
"user/id": 1, // with slash
"用户ID": 1, // CJK characters
"user🎉id": 1, // emoji
"user\nid": 1, // escaped newline in the key
"": 1 // the empty string is a valid key
}Legal does not mean usable. Most JSON consumers — code generators, schema validators, logging pipelines, metric flatteners — assume keys look like programming identifiers. Spaces break dotted-path tools (jq, JSONPath, OpenTelemetry attribute keys). Dots break flattening logic that uses dots as path separators. Slashes confuse tools that interpret keys as URL fragments. Emoji and CJK characters work fine in modern parsers but fail at the human-readability bar in code review.
Two specific edge cases worth knowing: duplicate keys in a JSON object are syntactically allowed by RFC 8259 but the behavior on parse is implementation-defined (most parsers keep the last one). The empty string key "" is legal and you will occasionally see it in data dumps from systems that lost a field name during transformation — every parser handles it but no language supports it idiomatically as a property name. For new schemas, restrict your keys to letters, digits, and one separator.
Translating between conventions on the wire
When your internal code uses one convention and the wire uses another, the right architecture is a single translation point — not scattered renames at every call site. Two npm packages cover the common case for TypeScript projects: camelcase-keys for incoming snake_case payloads, and snakecase-keys for outgoing requests.
// fetch wrapper with automatic case translation
import camelcaseKeys from 'camelcase-keys'
import snakecaseKeys from 'snakecase-keys'
export async function apiFetch<T>(
path: string,
init?: { method?: string; body?: unknown }
): Promise<T> {
const body = init?.body
? JSON.stringify(snakecaseKeys(init.body as Record<string, unknown>, { deep: true }))
: undefined
const res = await fetch(`https://api.example.com${path}`, {
method: init?.method ?? 'GET',
headers: { 'content-type': 'application/json' },
body,
})
const raw = await res.json()
return camelcaseKeys(raw, { deep: true }) as T
}
// Call sites speak camelCase only
const user = await apiFetch<User>('/users/1')
// user.firstName, user.createdAt — even though wire is first_name, created_atFor type-level accuracy, pair the runtime transform with a TypeScript mapped type that converts snake_case string literals to camelCase at the type system level.
// TypeScript template literal type for snake → camel
type SnakeToCamel<S extends string> =
S extends `${infer P}_${infer Q}${infer R}`
? `${P}${Uppercase<Q>}${SnakeToCamel<R>}`
: S
type CamelKeys<T> = {
[K in keyof T as K extends string ? SnakeToCamel<K> : K]: T[K]
}
// Wire type (matches the API)
type UserWire = {
user_id: number
first_name: string
created_at: string
}
// Internal type (auto-derived)
type User = CamelKeys<UserWire>
// = { userId: number; firstName: string; createdAt: string }For Express or Next.js route handlers that consume external APIs and re-emit JSON, the same pattern works as middleware:
// Express middleware: convert response bodies to camelCase
import camelcaseKeys from 'camelcase-keys'
app.use((req, res, next) => {
const originalJson = res.json.bind(res)
res.json = (body: unknown) => {
if (body && typeof body === 'object') {
return originalJson(camelcaseKeys(body as Record<string, unknown>, { deep: true }))
}
return originalJson(body)
}
next()
})The general rule: translate once at the IO boundary, never in business logic. If you find yourself renaming fields inside a service method, the translation layer is in the wrong place.
Style guides: Google JSON Style Guide, OpenAPI, JSON:API
Three published style guides shape what teams actually adopt. None is binding — they are reference points, not standards — but they show up in API design reviews often enough to be worth knowing.
Google JSON Style Guide. The original lives at google.github.io/styleguide/jsoncstyleguide.xmland was written for Google's web APIs. It specifies camelCase property names, lowercase enum values, and a small set of reserved property names (kind, fields, items, id, error). The style guide is followed by Google Maps, YouTube Data API, and many external teams who adopt it wholesale. It is not followed by Google Cloud REST APIs, which are protobuf-generated and use snake_case by default.
OpenAPI Specification. OpenAPI itself does not mandate a case style for JSON payloads — it describes the API; it does not prescribe the field names. But the OpenAPI tooling ecosystem assumes you will pick a style and stick with it. Spectral, the most popular OpenAPI linter, ships a property-name-format rule you can configure to enforce camelCase or snake_case across every schema in your spec.
# .spectral.yaml — enforce camelCase property names
extends: ["@stoplight/spectral:oas"]
rules:
property-name-camelcase:
description: Property names must be camelCase
severity: error
given: $.components.schemas..properties.*~
then:
function: pattern
functionOptions:
match: "^[a-z][a-zA-Z0-9]*$"JSON:API. The JSON:API spec restricts member names to lowercase letters, numbers, hyphens, underscores, and spaces. Older versions of the spec recommended hyphenated names (first-name); the recommendation was softened in later revisions because almost no implementations followed it. If you are building a JSON:API server today, pick snake_case or camelCase based on your client ecosystem — most current JSON:API tooling assumes one of those two.
For a deeper look at JSON:API specifically, see our JSON:API spec guide. For broader REST conventions, see JSON API design and REST API response format.
Decision matrix: when to use which
No single answer fits every API. The right convention depends on who reads your JSON, who you want to look like, and how much translation tooling you are willing to maintain. The table below summarizes the trade-offs.
| Convention | Best fit | Team size | Pros | Cons |
|---|---|---|---|---|
camelCase | JS/TS-first APIs, internal services with single language, GraphQL APIs | Any | Zero-friction in JS/TS; matches Microsoft Graph, Vercel; reads naturally in mobile SDKs (Swift, Kotlin) | Friction for Python/Ruby clients; needs translation for protobuf-derived schemas |
snake_case | Public APIs with mixed clients, ML/data APIs, payment APIs, anything Python-heavy | Any | Native fit for Python, Ruby, Rust; matches GitHub, Stripe, OpenAI, Anthropic; protobuf-friendly | Friction for JS/TS clients; requires camelcase-keys at the boundary |
kebab-case | Almost never inside JSON bodies — use for URL paths, HTML attributes, CSS only | — | Reads well in URL contexts; consistent with web standards in non-JSON contexts | Requires bracket notation in every major language; breaks code generators; no major API uses it |
PascalCase | Only when matching AWS IAM, CloudFormation, or .NET-heritage upstream APIs | Small to medium | Matches .NET property convention exactly; no rename needed in C# clients | Looks foreign in JS/Python codebases; signals legacy origin; almost no new APIs adopt it |
Quick heuristics for the common cases:
- New public API, mixed audience:
snake_case. It matches the modal large public API and the language tooling is widely available in every direction. - Internal service, TypeScript-only:
camelCase. Skip the translation layer entirely. - API designed to mirror a database: match the database column convention (almost always snake_case in Postgres/MySQL, PascalCase in SQL Server).
- API where most consumers integrate with Stripe/GitHub/OpenAI first:
snake_case— your consumers' existing translation layer covers you for free. - GraphQL API:
camelCase— the GraphQL convention is camelCase and almost every code generator assumes it. - You inherited a mixed codebase: document the existing dominant style, lint new fields against it, and translate at the boundary rather than refactoring the wire format.
For more on the language-side picture, see TypeScript JSON types, which covers the type-level case-conversion patterns in detail. For character-level concerns inside JSON strings, see String escaping and UTF-8 encoding.
Key terms
- camelCase
- A naming convention where the first letter is lowercase and each subsequent word starts with a capital letter, with no separator (
firstName,userId). Standard in JavaScript, TypeScript, Java, and most mobile platforms. - snake_case
- A naming convention where words are lowercase and joined by underscores (
first_name,user_id). Standard in Python, Ruby, Rust, and most large public JSON APIs (GitHub, Stripe, Twitter v2, OpenAI). - kebab-case
- A naming convention where words are lowercase and joined by hyphens (
first-name,user-id). Common in URL paths and CSS; legal but rare in JSON because every major language requires bracket notation to access hyphenated keys. - PascalCase
- A naming convention where the first letter of every word, including the first, is capitalized with no separator (
FirstName,UserId). Standard in .NET; survives in AWS IAM and CloudFormation JSON. - property name
- The string that identifies a value inside a JSON object. RFC 8259 calls these "names" or "member names"; most programming literature calls them "keys" or "properties".
- case translation
- The process of converting JSON keys from one convention to another at the IO boundary — typically snake_case on the wire to camelCase in client code. Standard packages:
camelcase-keysandsnakecase-keysfor npm;humpsfor Python.
Frequently asked questions
Should JSON keys use camelCase or snake_case?
There is no JSON spec answer — RFC 8259 says nothing about key naming style, and both are valid JSON. The practical answer depends on your primary consumers. If most of the code reading your JSON is JavaScript or TypeScript, camelCase matches the language conventions and avoids a translation layer (response.userId reads naturally; response.user_id requires bracket notation or a destructure rename). If most readers are Python, Ruby, Rust, or shell scripts, snake_case is what those ecosystems expect — user_id maps to a Python attribute without renaming. For server-to-server APIs with mixed clients, snake_case is the safer default because it survives every language unchanged and most large public APIs (GitHub, Stripe, Twitter v2, Google) ship snake_case for that reason. For internal services with a single client language, match the language.
What naming convention does Google use for JSON APIs?
Google's official JSON Style Guide specifies camelCase for property names — lowercase first letter, capital first letter of each subsequent word, no separators. That is the rule on paper. In practice Google ships snake_case in many of its largest APIs (the Google Cloud REST APIs, BigQuery, Firestore over REST) because they are generated from Protocol Buffers, which uses snake_case by default and translates to snake_case in the JSON encoder unless you opt into the camelCase mapping. The Google JSON Style Guide is followed by some Google web APIs (Maps, YouTube Data API) and by many external teams who adopt it as a baseline. If you're choosing a convention because Google does X, look at the specific Google product you're integrating with rather than assuming uniformity.
Is kebab-case allowed in JSON?
Yes — JSON keys are strings, and a string can contain hyphens, dots, spaces, or any other Unicode character. {"user-id": 1} parses cleanly with every conforming JSON parser. The problem is consumption, not parsing. In JavaScript, you cannot access a kebab-case key with dot notation because the hyphen is the subtraction operator: response.user-id is parsed as response.user minus the variable id. You must use bracket notation (response["user-id"]) or destructure with a rename ({ "user-id": userId } = response). The same friction exists in most other languages — Python, Ruby, Go, Rust — none of them allow hyphens in identifiers. kebab-case is legal in JSON but creates real ergonomic costs for every reader, which is why almost no production API uses it for object keys (it shows up in URL paths and CSS, not JSON bodies).
How do I handle mixed conventions when integrating with multiple APIs?
Pick one convention for your internal code and translate at the boundary. If your codebase is TypeScript, the standard pattern is camelCase internally with snake_case translation at the HTTP layer — use camelcase-keys when you receive a response and snakecase-keys before you send a request. Centralize this in a single fetch wrapper or API client so every call site sees camelCase types. For schemas that vary per upstream API, write narrow adapter modules (one per integration) that convert payloads into your internal shape. Avoid mixing conventions in the same object — { userId, created_at } is harder to reason about than either pure style. If you use a generator (OpenAPI codegen, Prisma, Zod), configure it to emit your house style and translate at the IO boundary, not throughout the codebase.
Does JSON:API specify a naming convention?
JSON:API does not mandate a specific case style, but it does have rules about what characters are allowed in member names. The spec restricts member names to lowercase letters, numbers, and three separator characters: hyphen (-), underscore (_), and the space character (which is allowed but discouraged). It calls hyphenated names a 'preferred' style in older versions of the spec, but later editions softened this and most JSON:API implementations in the wild use either snake_case or camelCase rather than kebab-case. If you're building a JSON:API server, the practical advice is: pick snake_case or camelCase based on your client ecosystem, document the choice in your API docs, and skip the older kebab-case recommendation unless you have a specific reason to revive it.
Can JSON keys contain spaces or special characters?
Technically yes — JSON keys are strings, so any valid JSON string is a valid key. {"first name": "Ada", "email@home": "..."} parses without error. The technical answer ends there; the practical answer is no, you should not do this. Spaces and special characters force every consumer into bracket notation or quoted access, break most code generators, confuse JSON Schema validators that expect identifier-like names, and trip up logging and metrics tools that flatten JSON into dot paths. The only places you see space-keyed JSON in production are configuration files designed for humans to type by hand (where readability beats programmability) and historical APIs that committed early and cannot break their schema. For new APIs, stick to letters, digits, and a single separator character.
Should I use PascalCase for JSON keys?
PascalCase (FirstNameLikeThis) is unusual for JSON keys outside the Microsoft and AWS ecosystems. Microsoft's older .NET APIs and parts of AWS (IAM policies, CloudFormation templates, some S3 responses) ship PascalCase because it matches the C# property convention and the original AWS SDK design. Newer Microsoft Graph and most AWS service APIs (DynamoDB, Lambda invocation responses) use camelCase. For a new API, PascalCase carries a cost: it looks foreign in JavaScript and Python codebases and signals 'we generated this from C#' even when you did not. Use PascalCase only when you're shipping into an existing .NET-heavy ecosystem where consumers expect it, or when you're matching an upstream API's surface (AWS IAM is a common forcing function). Otherwise default to camelCase or snake_case.
How do I auto-translate snake_case to camelCase in TypeScript?
Two ways. At runtime, use the camelcase-keys npm package: import camelcaseKeys from 'camelcase-keys' and call camelcaseKeys(jsonResponse, { deep: true }) to recursively convert every key. The reverse package is snakecase-keys for outgoing requests. Wrap both in a single fetch helper so every call site sees camelCase and the wire stays snake_case. At the type level, TypeScript template literal types can express the conversion as a type — type CamelCase<S extends string> = S extends `${infer P}_${infer Q}${infer R}` ? `${P}${Uppercase<Q>}${CamelCase<R>}` : S — which gives you accurate compile-time types when paired with a runtime transformer. For larger projects use a code generator (OpenAPI Generator with the camelCase plugin, or Prisma with @map) so both the types and the runtime transform stay in sync without hand-written mappers.
Further reading and primary sources
- Google JSON Style Guide — The canonical reference for camelCase JSON keys and reserved property names
- JSON:API Specification — Member Names — Allowed characters and the historical hyphen recommendation
- Spectral — OpenAPI Linter — Enforce property-name rules across an OpenAPI spec in CI
- camelcase-keys (npm) — Recursive snake_case-to-camelCase converter for JS/TS HTTP clients
- RFC 8259 — JSON — The JSON standard — silent on key naming, defines what a JSON string can contain