Unifying Python and RESTful API Naming Conventions with Pydantic
2025-02-05

This post is also available in Chinese.

Every Python backend developer has had this conversation: you write item_id in your code, the API returns item_id in JSON, and then a frontend colleague asks — "Can you change that to itemId? We use camelCase in JavaScript."

You can either manually map every field (tedious and error-prone), or make the frontend deal with underscores (ugly in JS). Neither option is great.

Pydantic solves this with a single config line.


The Problem

Two naming conventions, both perfectly reasonable:

Both are standard in their ecosystems. Forcing either side to adopt the other's convention creates friction. The right approach: keep snake_case in your Python code, expose camelCase in your API, and let Pydantic handle the conversion automatically.

Conversion flow from Python snake_case to API camelCase via Pydantic

GET Requests: Use Query's alias Parameter

GET parameters come through the URL query string, and there are usually just a few. For this, use Query with alias to map each one:

from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/items/")
async def get_item(
    item_id: int = Query(..., alias="itemId"),
    search_query: str | None = Query(None, alias="searchQuery"),
):
    return {"item_id": item_id, "search_query": search_query}

The frontend calls:

GET /items/?itemId=42&searchQuery=example

Inside the handler, you get item_id=42 and search_query="example" — clean PEP 8 names throughout your Python code.

Response:

{
  "item_id": 42,
  "search_query": "example"
}

Note that the response JSON still uses snake_case here. If you want the response in camelCase too, you'll need a response_model backed by a Pydantic model — which we'll cover next.


POST Requests: Use alias_generator for Bulk Conversion

POST request bodies typically have more fields. Writing alias for each one is tedious. Pydantic's alias_generator lets you configure it once at the model level and every field converts automatically:

from fastapi import FastAPI
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel

app = FastAPI()

class ItemCreate(BaseModel):
    model_config = ConfigDict(
        alias_generator=to_camel,
        populate_by_name=True,
    )

    item_id: int = Field(..., description="The ID of the item")
    search_query: str | None = Field(None, max_length=50, description="A search query")
    created_at: str | None = Field(None, description="Creation timestamp")

@app.post("/items/")
async def create_item(item: ItemCreate):
    return item

Three key settings:

  1. alias_generator = to_camel: Automatically maps item_id to itemId, search_query to searchQuery, and so on for every field.
  2. populate_by_name = True: Allows both snake_case and camelCase for input. Without this, only camelCase would be accepted.
  3. to_camel: A built-in conversion function from pydantic.alias_generators.

Frontend sends a POST:

POST /items/
Content-Type: application/json

{
  "itemId": 42,
  "searchQuery": "example",
  "createdAt": "2025-02-05T10:00:00Z"
}

In the handler, item.item_id is 42, item.search_query is "example" — snake_case all the way through your Python code.

Response (since alias_generator also affects serialization, it outputs camelCase automatically):

{
  "itemId": 42,
  "searchQuery": "example",
  "createdAt": "2025-02-05T10:00:00Z"
}

Making GET Responses camelCase Too

As mentioned, a plain return {"item_id": ...} from a GET handler produces snake_case in the response. To unify it, add a response_model:

class ItemResponse(BaseModel):
    model_config = ConfigDict(
        alias_generator=to_camel,
        populate_by_name=True,
    )

    item_id: int
    search_query: str | None = None

@app.get("/items/", response_model=ItemResponse)
async def get_item(
    item_id: int = Query(..., alias="itemId"),
    search_query: str | None = Query(None, alias="searchQuery"),
):
    return ItemResponse(item_id=item_id, search_query=search_query)

Now the GET response is camelCase too:

{
  "itemId": 42,
  "searchQuery": "example"
}

Extract a Base Class

If multiple models in your project need camelCase conversion, repeating model_config everywhere gets old fast. Extract a base class:

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

class CamelModel(BaseModel):
    """Base model for all models that need camelCase API serialization."""
    model_config = ConfigDict(
        alias_generator=to_camel,
        populate_by_name=True,
    )

# Just inherit from it
class ItemCreate(CamelModel):
    item_id: int
    search_query: str | None = None
    created_at: str | None = None

class UserProfile(CamelModel):
    user_name: str
    email_address: str
    is_active: bool = True

Every model inheriting CamelModel gets camelCase aliases for free.


Gotchas

A few things that can trip you up:

  1. by_alias when serializing manually: If you call model.model_dump() yourself, it outputs snake_case by default. For camelCase, use model.model_dump(by_alias=True). FastAPI's response_model handles this automatically.

  2. Nested models: alias_generator only affects the current model's fields, not nested ones. Nested models need their own alias_generator config — another reason to use the base class approach.

  3. ORM integration: If you use SQLAlchemy or similar, keep ORM models in snake_case and let Pydantic models handle the conversion. Don't mix naming conventions at the ORM layer.

  4. OpenAPI docs: With alias_generator configured, FastAPI's auto-generated Swagger UI shows camelCase field names. Frontend developers see exactly the names they need — no guesswork.


Summary

The rule is simple: write snake_case in Python, expose camelCase in your API, let Pydantic bridge the gap.

The practical setup:

Both sides write idiomatic code, and Pydantic handles the translation in between.