写 Python 后端的人都知道一个别扭的事:你在代码里写 item_id,前端拿到的 JSON 也是 item_id,但前端同事会过来问你——"能不能改成 itemId?我们 JavaScript 都是 camelCase。"
改?每个字段都手动映射一遍,累。不改?前端代码里全是下划线,不伦不类。
Pydantic 可以一行配置解决这个问题。
问题在哪
两套命名规范各有各的道理:
- Python (PEP 8):
snake_case——item_id,search_query,created_at - JavaScript / REST API:
camelCase——itemId,searchQuery,createdAt
都是各自社区的标准,谁也说服不了谁。硬要前端写 item_id 或者后端写 itemId,都是在给一边找不痛快。
正确的做法是:后端代码里继续写 snake_case,API 对外暴露 camelCase,中间让 Pydantic 自动转换。
GET 请求:用 Query 的 alias 参数
GET 请求的参数在 URL 查询字符串里,数量通常不多。这种场景用 Query 的 alias 参数逐个映射就行:
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}
前端这样调:
GET /items/?itemId=42&searchQuery=example
后端函数里拿到的就是 item_id=42、search_query="example",完全符合 PEP 8。
响应结果:
{
"item_id": 42,
"search_query": "example"
}
这里有个细节——响应的 JSON 字段名还是 snake_case。如果你想让响应也变成 camelCase,需要用 response_model 配合 Pydantic 模型,下一节就会讲到。
POST 请求:用 alias_generator 批量转换
POST 请求的 body 通常字段更多,一个一个写 alias 太麻烦。Pydantic 提供了 alias_generator,在模型配置里写一次,所有字段自动转换:
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
三个关键配置:
alias_generator = to_camel:自动把item_id映射为itemId,search_query映射为searchQuery,所有字段一步到位。populate_by_name = True:允许同时使用 snake_case 和 camelCase 来传值。没有这个配置,只能用 camelCase。to_camel:Pydantic 内置的转换函数,从pydantic.alias_generators导入。
前端发 POST 请求:
POST /items/
Content-Type: application/json
{
"itemId": 42,
"searchQuery": "example",
"createdAt": "2025-02-05T10:00:00Z"
}
后端收到后,item.item_id 就是 42,item.search_query 就是 "example"——Python 代码里始终是 snake_case。
响应结果(因为 alias_generator 也影响序列化,会自动用 camelCase 输出):
{
"itemId": 42,
"searchQuery": "example",
"createdAt": "2025-02-05T10:00:00Z"
}
让 GET 的响应也变成 camelCase
前面说了,GET 请求直接 return {"item_id": ...} 的话,响应是 snake_case。要统一的话,加一个 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)
这样 GET 请求的响应也会自动序列化为 camelCase:
{
"itemId": 42,
"searchQuery": "example"
}
抽个基类,别重复写
如果项目里很多模型都需要 camelCase 转换,每个都写一遍 model_config 太啰嗦。抽一个基类:
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel
class CamelModel(BaseModel):
"""所有需要 camelCase API 风格的模型都继承这个。"""
model_config = ConfigDict(
alias_generator=to_camel,
populate_by_name=True,
)
# 之后直接继承就行
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
所有继承 CamelModel 的模型自动就有 camelCase 的 alias。干净。
注意事项
几个容易踩的坑:
by_alias参数:如果你手动调用model.model_dump()序列化,默认输出 snake_case。要输出 camelCase,需要model.model_dump(by_alias=True)。FastAPI 的response_model会自动处理这个。嵌套模型:
alias_generator只影响当前模型的字段名,不会递归转换嵌套模型。嵌套的模型也需要配置alias_generator,所以用基类继承的方式最省事。和 ORM 配合:如果你用 SQLAlchemy 之类的 ORM,ORM 模型和 Pydantic 模型是分开的。ORM 用 snake_case,Pydantic 模型负责转换,不要在 ORM 层做命名转换。
OpenAPI 文档:配了
alias_generator之后,FastAPI 自动生成的 Swagger 文档里会显示 camelCase 的字段名,前端看文档直接就是他们需要的字段名。
小结
思路就一句:Python 代码里写 snake_case,API 对外暴露 camelCase,中间让 Pydantic 桥接。
具体怎么落地:
- GET 请求参数少,用
Query(alias="camelCase")逐个映射 - POST 请求体字段多,用
alias_generator = to_camel批量转换 - 加
populate_by_name = True让两种命名都能传值 - 抽一个
CamelModel基类,全项目复用
这样前后端各写各的风格,谁都不用迁就谁。