FastAPI 如何利用 Pydantic 做参数校验和响应序列化?
简化版
FastAPI 通过 Pydantic 模型和类型注解自动校验请求数据,并把校验失败转换成 422 响应。返回数据时,可以用 response_model 控制响应结构、过滤字段、做序列化,让接口输入输出都有明确契约。
详细版
FastAPI 的请求校验主要依赖 Python 类型注解和 Pydantic:
- 路径参数、查询参数根据函数参数类型校验;
- 请求体通常用 Pydantic
BaseModel定义; - 字段约束可以用
Field、Query、Path、Body等声明; - 校验失败时 FastAPI 自动返回结构化错误;
response_model可以约束返回结构,避免泄漏敏感字段。
示例:
from pydantic import BaseModel, Field
class UserCreate(BaseModel):
name: str = Field(min_length=2, max_length=30)
age: int = Field(ge=0, le=150)
@app.post("/users", response_model=UserOut)
def create_user(user: UserCreate):
return save_user(user)
面试重点是:Pydantic 不只是类型提示,它参与运行时校验和序列化。
完整版教学
一、类型注解在 FastAPI 中不是装饰品
在普通 Python 代码里,类型注解很多时候只用于静态检查和 IDE 提示,运行时不会自动强制类型。但在 FastAPI 中,类型注解会被框架读取,用来决定参数来自哪里、如何校验、如何生成文档。
比如:
@app.get("/items/{item_id}")
def get_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
item_id: int 表示路径参数必须能转换成整数。如果用户访问 /items/abc,FastAPI 会返回校验错误,而不是让业务代码自己判断。q 因为有默认值 None,所以是可选查询参数。
这就是 FastAPI 的核心体验:函数签名本身就是接口契约。
二、请求体为什么通常用 Pydantic 模型
当请求体是 JSON 对象时,一般用 Pydantic 模型表达结构:
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
这个模型会告诉 FastAPI:请求体必须包含 username、email、password,并且 email 要符合邮箱格式。校验通过后,路由函数拿到的是一个 UserCreate 对象,而不是原始 dict。
这样写有三个好处。第一,业务代码更干净,不需要反复从字典里取值。第二,字段约束集中在模型里,便于复用。第三,OpenAPI 文档会自动展示请求体 schema。
三、Field、Query、Path 的作用
Pydantic 的 Field 常用于模型字段约束:
class ProductCreate(BaseModel):
name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0)
FastAPI 的 Query、Path、Body 则用于声明请求参数来源和约束:
from fastapi import Query, Path
@app.get("/products/{product_id}")
def get_product(
product_id: int = Path(gt=0),
keyword: str | None = Query(default=None, max_length=50),
):
return {"product_id": product_id, "keyword": keyword}
这些声明不仅用于运行时校验,也会进入接口文档。面试时可以强调:FastAPI 的参数声明同时服务于校验、文档和可读性。
四、422 错误说明了什么
FastAPI 参数校验失败时,通常返回 HTTP 422。422 表示请求格式语法上可解析,但语义上不符合接口要求。例如 JSON 是合法的,但字段类型不对、必填字段缺失、数值超出范围。
这和 400 有区别。400 更偏向请求本身不合法,比如 JSON 语法错误。FastAPI 默认用 422 表达校验失败,这是符合很多 API 设计习惯的。
返回的错误结构会指出错误位置、错误类型和错误信息。前端或调用方可以根据这些信息定位字段问题。
五、response_model 的价值
很多人只关注请求校验,忽略响应序列化。事实上 response_model 非常重要:
class UserOut(BaseModel):
id: int
username: str
email: str
@app.get("/users/{user_id}", response_model=UserOut)
def get_user(user_id: int):
return {
"id": user_id,
"username": "tom",
"email": "tom@example.com",
"password_hash": "secret",
}
即使返回对象里有 password_hash,响应也会按 UserOut 过滤掉。这能降低敏感字段泄漏风险。
response_model 还可以把 ORM 对象、字典或 Pydantic 对象统一序列化成符合 schema 的响应。对于大型项目,输入模型、输出模型、数据库模型通常要分开,不要直接把数据库模型暴露给外部。
六、Pydantic v1 和 v2 要注意差异
FastAPI 在较新版本中支持 Pydantic v2。Pydantic v2 在配置方式、校验器写法、序列化方法上和 v1 有差异。例如 v1 常见 orm_mode = True,v2 中更常见 model_config = ConfigDict(from_attributes=True)。
面试不一定深挖版本细节,但工程上要注意项目使用的 FastAPI 和 Pydantic 版本。复制旧代码时,如果版本不一致,可能出现模型配置失效、校验器不运行等问题。
七、常见误区与追问
| 场景 | 推荐做法 | 说明 |
|---|---|---|
| 创建用户 | UserCreate 输入模型 | 包含密码等写入字段 |
| 返回用户 | UserOut 响应模型 | 不包含 password_hash、内部状态 |
| 数据库存储 | ORM Model | 负责表结构和持久化,不直接作为外部契约 |
class UserCreate(BaseModel):
email: EmailStr
password: str = Field(min_length=8)
class UserOut(BaseModel):
id: int
email: EmailStr
@app.post("/users", response_model=UserOut)
def create_user(user: UserCreate):
saved = save_user(user)
return saved
记忆钩子:Pydantic 在 FastAPI 里负责“入口校验、出口塑形”,不是负责“落库保存”。
第一个误区是把 Pydantic 当 ORM。Pydantic 是数据校验和序列化工具,不负责数据库持久化。
第二个误区是输出直接返回数据库实体。这样容易泄漏内部字段,也让外部 API 和数据库结构强耦合。
第三个误区是过度依赖自动类型转换。例如字符串 "123" 能被转成整数,但对于一些严格业务场景,可能需要更明确的校验规则。
- 误区:把 Pydantic 当 ORM。 Pydantic 做校验、转换和序列化,不管理连接、事务、查询和持久化。
- 误区:直接把 ORM 实体作为对外响应。 ORM 模型可能包含密码哈希、删除标记、内部字段,应该用
response_model单独定义输出契约。 - 误区:只依赖宽松类型转换。
"123"可以被转成整数,但金额、权限、枚举、业务状态往往需要Field、枚举类型或自定义校验器明确约束。 - 追问:422 和 400 怎么区分? 422 常表示请求语法可解析但字段语义不符合模型约束;400 更偏向请求本身格式错误或通用客户端错误。
- 追问:Pydantic v1 到 v2 常见迁移点是什么? v2 更常用
model_dump()、model_validate()、ConfigDict(from_attributes=True),旧的dict()、parse_obj()、orm_mode代码要结合版本核对。 - 追问:为什么输入模型和输出模型要分开? 输入关注客户端能提交什么,输出关注服务端允许暴露什么,二者字段、安全边界和演进节奏都不同。
八、加强记忆
记 Pydantic 在 FastAPI 里的作用,可以抓住“入口校验、出口塑形”这六个字:请求进来时按模型和类型注解校验,响应出去时按 response_model 序列化和过滤。它让接口契约集中在代码里,同时自动生成文档。