One of the most common challenges when working with APIs is the naming convention mismatch between JavaScript's camelCase and Python's snake_case. JSON from JavaScript APIs typically uses camelCase keys, while Python code expects snake_case. Here's how to handle this conversion cleanly and efficiently.
The Problem: Two Worlds Colliding
When a JavaScript frontend sends data to a Python backend, or when you consume a third-party API designed by JavaScript developers, you get JSON that looks like this:
{
"userId": 12345,
"firstName": "Alice",
"lastName": "Smith",
"emailAddress": "alice@example.com",
"createdAt": "2026-01-29T10:00:00Z",
"isActive": true
} But Python code following PEP 8 expects snake_case:
user = {
"user_id": 12345,
"first_name": "Alice",
"last_name": "Smith",
"email_address": "alice@example.com",
"created_at": "2026-01-29T10:00:00Z",
"is_active": True
} You need a clean way to transform between these formats without manually rewriting every key.
Quick Conversion
Transform field names between JavaScript and Python formats
Solution 1: Manual Transformation (Small Objects)
For small objects or one-time conversions, manual mapping is straightforward:
import requests
# Get data from API
response = requests.get('https://api.example.com/user/12345')
api_data = response.json()
# Manual transformation
user = {
'user_id': api_data['userId'],
'first_name': api_data['firstName'],
'last_name': api_data['lastName'],
'email_address': api_data['emailAddress'],
'created_at': api_data['createdAt'],
'is_active': api_data['isActive']
}
# Now use Pythonic snake_case
print(f"User {user['first_name']} {user['last_name']}") Pros and Cons
- ✓ Simple and explicit
- ✓ No dependencies
- ✓ Easy to debug
- ✗ Tedious for large objects
- ✗ Error-prone (typos, missed fields)
- ✗ Not reusable
Solution 2: Recursive Conversion Function
For production code, write a reusable conversion function:
import re
def camel_to_snake(name):
"""Convert camelCase string to snake_case."""
# Insert underscore before uppercase letters
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
# Handle acronyms (e.g., HTTPResponse -> http_response)
name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', name)
return name.lower()
def convert_keys(obj):
"""
Recursively convert all camelCase keys to snake_case.
Handles nested dicts and lists.
"""
if isinstance(obj, dict):
return {
camel_to_snake(key): convert_keys(value)
for key, value in obj.items()
}
elif isinstance(obj, list):
return [convert_keys(item) for item in obj]
else:
return obj
# Usage
api_response = {
"userId": 12345,
"firstName": "Alice",
"contactInfo": {
"emailAddress": "alice@example.com",
"phoneNumber": "+1234567890"
},
"recentOrders": [
{"orderId": 1, "totalPrice": 99.99},
{"orderId": 2, "totalPrice": 149.99}
]
}
python_data = convert_keys(api_response)
# Result:
# {
# "user_id": 12345,
# "first_name": "Alice",
# "contact_info": {
# "email_address": "alice@example.com",
# "phone_number": "+1234567890"
# },
# "recent_orders": [
# {"order_id": 1, "total_price": 99.99},
# {"order_id": 2, "total_price": 149.99}
# ]
# } Handling Edge Cases
The regex approach handles common cases like:
userId→user_idHTTPResponse→http_responseXMLParser→xml_parsergetAPIKey→get_api_key
Solution 3: Using Pydantic (Recommended)
Pydantic is the modern Python solution for API data validation and conversion. It automatically handles camelCase to snake_case:
from pydantic import BaseModel, Field
from datetime import datetime
class User(BaseModel):
user_id: int = Field(alias='userId')
first_name: str = Field(alias='firstName')
last_name: str = Field(alias='lastName')
email_address: str = Field(alias='emailAddress')
created_at: datetime = Field(alias='createdAt')
is_active: bool = Field(alias='isActive')
class Config:
# Allow population by field name or alias
populate_by_name = True
# Parse JSON directly
json_data = '''{
"userId": 12345,
"firstName": "Alice",
"lastName": "Smith",
"emailAddress": "alice@example.com",
"createdAt": "2026-01-29T10:00:00Z",
"isActive": true
}'''
user = User.parse_raw(json_data)
# Access with Pythonic snake_case
print(user.first_name) # "Alice"
print(user.user_id) # 12345
# Convert back to camelCase for API responses
print(user.dict(by_alias=True))
# {"userId": 12345, "firstName": "Alice", ...} Pydantic with Automatic Alias Generation
For large models, use automatic alias generation:
from pydantic import BaseModel, ConfigDict
def to_camel(string: str) -> str:
"""Convert snake_case to camelCase."""
components = string.split('_')
return components[0] + ''.join(x.title() for x in components[1:])
class User(BaseModel):
model_config = ConfigDict(
alias_generator=to_camel,
populate_by_name=True
)
# Define in snake_case - aliases auto-generated
user_id: int
first_name: str
last_name: str
email_address: str
is_active: bool
# Pydantic automatically creates:
# user_id → userId
# first_name → firstName
# etc.Related Resources
Solution 4: Using dataclasses with dacite
If you prefer Python's built-in dataclasses, combine them with dacite for conversion:
from dataclasses import dataclass
from dacite import from_dict, Config
import re
@dataclass
class User:
user_id: int
first_name: str
last_name: str
email_address: str
is_active: bool
def camel_to_snake(name):
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
# API data in camelCase
api_data = {
"userId": 12345,
"firstName": "Alice",
"lastName": "Smith",
"emailAddress": "alice@example.com",
"isActive": True
}
# Convert keys
snake_data = {
camel_to_snake(k): v
for k, v in api_data.items()
}
# Create dataclass
user = from_dict(User, snake_data)
print(user.first_name) # "Alice"Bi-Directional Conversion
Often you need to convert both ways - receiving camelCase from an API and sending it back:
def snake_to_camel(name):
"""Convert snake_case to camelCase."""
components = name.split('_')
return components[0] + ''.join(x.title() for x in components[1:])
def convert_keys_to_camel(obj):
"""Convert all snake_case keys to camelCase."""
if isinstance(obj, dict):
return {
snake_to_camel(key): convert_keys_to_camel(value)
for key, value in obj.items()
}
elif isinstance(obj, list):
return [convert_keys_to_camel(item) for item in obj]
else:
return obj
# Receive from API (camelCase → snake_case)
api_data = {"userId": 123, "firstName": "Bob"}
python_data = convert_keys(api_data)
# {"user_id": 123, "first_name": "Bob"}
# Process in Python...
python_data["last_name"] = "Jones"
# Send back to API (snake_case → camelCase)
api_payload = convert_keys_to_camel(python_data)
# {"userId": 123, "firstName": "Bob", "lastName": "Jones"}
requests.post('https://api.example.com/users', json=api_payload)Performance Considerations
For Small Objects
Any approach works fine. Use the simplest solution.
For Large Objects (1000+ keys)
Pydantic is fastest due to compiled C extensions:
# Benchmark (1000 users, 10 fields each)
# Manual dict comprehension: ~50ms
# Pydantic: ~30ms (40% faster)
# Recursive function: ~45ms For High-Frequency Conversions
Cache the conversion function or use Pydantic models as singletons.
Best Practices
1. Be Consistent
Pick one conversion method and use it throughout your codebase. Don't mix manual mapping with automatic conversion.
2. Validate Data
Always validate API responses. Pydantic does this automatically:
class User(BaseModel):
user_id: int # Validates it's an integer
email_address: str # Validates it's a string
age: int = Field(gt=0, lt=150) # Age validation 3. Handle Nested Objects
Ensure your conversion handles nested dictionaries and lists:
api_response = {
"userData": {
"userId": 123,
"contactInfo": {
"emailAddress": "test@example.com"
}
},
"orderHistory": [
{"orderId": 1, "totalPrice": 99.99}
]
} 4. Document the Conversion
Make it clear when conversion happens:
def fetch_user(user_id: int) -> dict:
"""
Fetch user from API.
Returns:
dict: User data in snake_case format (converted from API's camelCase)
"""
response = requests.get(f'/users/{user_id}')
return convert_keys(response.json()) 5. Test Both Directions
def test_conversion():
# Test camelCase → snake_case
assert camel_to_snake("userId") == "user_id"
assert camel_to_snake("HTTPResponse") == "http_response"
# Test snake_case → camelCase
assert snake_to_camel("user_id") == "userId"
assert snake_to_camel("http_response") == "httpResponse"Real-World Example: REST API Client
from typing import List
from pydantic import BaseModel, Field, ConfigDict
def to_camel(string: str) -> str:
components = string.split('_')
return components[0] + ''.join(x.title() for x in components[1:])
class User(BaseModel):
model_config = ConfigDict(
alias_generator=to_camel,
populate_by_name=True
)
user_id: int
first_name: str
last_name: str
email: str
class APIClient:
def __init__(self, base_url: str):
self.base_url = base_url
def get_user(self, user_id: int) -> User:
"""Fetch user - automatically converts camelCase → snake_case"""
response = requests.get(f"{self.base_url}/users/{user_id}")
return User(**response.json())
def create_user(self, user: User) -> User:
"""Create user - automatically converts snake_case → camelCase"""
payload = user.model_dump(by_alias=True)
response = requests.post(
f"{self.base_url}/users",
json=payload
)
return User(**response.json())
# Usage
client = APIClient("https://api.example.com")
# Fetch - receives camelCase, converts to snake_case
user = client.get_user(123)
print(user.first_name) # Works with snake_case
# Update
user.first_name = "Jane"
# Send - converts snake_case back to camelCase
updated_user = client.create_user(user)Summary
Choose your solution based on your needs:
- Quick scripts: Manual mapping
- Simple projects: Recursive conversion function
- Production APIs: Pydantic (recommended)
- Dataclass fans: dataclasses + dacite
The key is handling the conversion cleanly so your Python code can stay Pythonic with snake_case while seamlessly integrating with camelCase APIs.