FastAPI 的「魔法」八成来自 Pydantic。请求进来的 JSON 之所以能自动变成带类型的对象、非法字段自动被挡在门外,全靠它。这篇把 Pydantic v2 的常用能力摊开讲,重点放在和 FastAPI 配合最高频的几块。
最基础的模型
from pydantic import BaseModel class User(BaseModel): id: int name: str email: str age: int | None = None
age: int | None = None 表示「可缺省、缺省为 None」。FastAPI 收到请求时,会用这个模型去解析 body:字段名对齐、类型转换、缺字段或类型错就返回 422。
注意 Python 基础类型和 Pydantic 类型不是一回事。age: int 在 Pydantic 里意味着「能转成 int 的都收」(字符串 "18" 会转成 18);如果你要的是「必须是 int 对象」这种语义,Pydantic 也支持严格模式,但默认宽松转换对 API 更友好。
字段约束:Field
光有类型还不够,业务常要「价格大于 0」「名字不超过 50 字」。用 Field 加约束:
from pydantic import BaseModel, Field class Item(BaseModel): name: str = Field(..., min_length=1, max_length=50) price: float = Field(..., gt=0, description="单价,必须为正") stock: int = Field(default=0, ge=0)
... 是 Pydantic 的「必填」标记,等价于「没有默认值」。常用约束:
gt/ge/lt/le:数值大于/大于等于/小于/小于等于min_length/max_length:字符串长度pattern:正则匹配(如邮箱、手机号格式)default:默认值;用Field(default=...)也能同时带约束
嵌套、列表与联合
真实数据结构很少是扁平的。Pydantic 支持任意嵌套:
class Address(BaseModel): city: str street: str class Order(BaseModel): items: list[str] # 字符串列表 addresses: list[Address] # 模型列表,逐项递归校验 note: str | None = None # 联合类型:字段可以是多种类型之一 class Id(BaseModel): value: int | str
addresses 里的每个元素都会按 Address 递归校验,某一项 city 缺失会精确报出路径 addresses.0.city。这种「深层错误定位」是手写校验很难做到的。
关键配置:ConfigDict
模型类里的 model_config = ConfigDict(...) 控制校验和序列化的行为。两个最常改的:
from pydantic import BaseModel, ConfigDict class UserOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int name: str
from_attributes=True(v1 叫orm_mode=True):允许从 ORM 对象(如 SQLAlchemy 行)读取属性,而不只是 dict。配合响应模型把数据库行转成 API 输出时必开。extra="ignore":收到多余字段时忽略而不是报错(默认extra="forbid"会拒收未知字段)。对接第三方数据时常用ignore。
请求模型与响应模型分离
这是 FastAPI 里一个反直觉但很重要的实践:进来的和出去的用不同模型。
class UserCreate(BaseModel): # 请求:创建用户要提交什么 name: str email: str password: str # 密码只进不出 class UserOut(BaseModel): # 响应:返回给用户看什么 id: int name: str email: str @app.post("/users/", response_model=UserOut) async def create_user(data: UserCreate): user = db.create(data) # 伪代码 return user
response_model=UserOut 做了两件事:一是过滤掉 password 这类敏感字段,即使 handler 返回的对象里有也不会外泄;二是生成响应结构的文档。这让「输入契约」和「输出契约」各自清晰,也天然避免了密码回传的安全坑。
序列化控制
把模型转成 dict / JSON 用这几个方法:
user = UserOut(id=1, name="宁", email="n@x.com") user.model_dump() # → dict user.model_dump(mode="json") # → 可 JSON 序列化的 dict(datetime 转字符串) user.model_dump(exclude={"email"}) # 排除字段
mode="json" 在处理时间、Decimal 这类非原生 JSON 类型时必用,否则直接 json.dumps 会报 TypeError。
一个完整例子
把前几篇的点串起来:路由收 UserCreate、用 Field 校验、落库后返回 UserOut、密码不外泄。
from fastapi import FastAPI from pydantic import BaseModel, Field, ConfigDict app = FastAPI() class UserCreate(BaseModel): name: str = Field(..., min_length=1, max_length=30) email: str = Field(..., pattern=r"^[^@]+@[^@]+\.[^@]+$") password: str = Field(..., min_length=6) class UserOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int name: str email: str @app.post("/users/", response_model=UserOut) async def create_user(data: UserCreate): # 伪代码:hash_password + 入库 return {"id": 1, "name": data.name, "email": data.email}
访问 /docs 时,文档里会显示请求体要 name/email/password、响应只有 id/name/email,约束条件一并展示。这正是「类型即文档」的落地。
下一篇讲异步数据库——前面这些模型最终要落到存储上。我会用 SQLAlchemy 2.0 的 async 写法,配合本篇的 from_attributes=True 把数据库行直接变响应模型。