FastAPI请求验证:10 个 Pydantic进阶技巧
FastAPI 接口一上线,最先炸的往往不是业务代码,是请求参数。
日志里一串 422 Unprocessable Entity,前端说“我传了”,后端说“我没收到”,最后翻出来一看,不是字段少了,就是类型被悄悄转了。
Pydantic 这东西,用浅了就是 BaseModel 加几个字段;用深一点,它其实是 FastAPI 请求入口的第一道闸门。面试问它,不是想听你背概念,是想看你知不知道线上那些脏请求怎么挡。
下面这 10 个点,我平时写 FastAPI 接口基本都会用到。
第一个,别什么都让 Pydantic 自动转。
很多人喜欢这样写:
from pydantic import BaseModelclassPayReq(BaseModel):
order_id: int
amount: float
看着没问题,但前端传 "1001",Pydantic 也给你转成 1001。有些场景能忍,有些场景不能忍。
比如订单号、手机号、外部流水号,这些字段我一般不让它乱转。
from pydantic import BaseModel, StrictStr, condecimalclassPayReq(BaseModel):
order_id: StrictStr
amount: condecimal(gt=0, max_digits=10, decimal_places=2)
金额也别用 float,这地方我第一眼就不信。支付、账单、积分兑换,能用 Decimal 就别用浮点数。
第二个,字段限制要写在模型里,别散在接口里。
from pydantic import BaseModel, FieldclassCouponReq(BaseModel):
user_id: str = Field(min_length=6, max_length=32)
coupon_code: str = Field(pattern=r"^[A-Z0-9]{8,16}$")
channel: str = Field(default="app")
接口里再写一堆 if not xxx,后面一定会散。今天这个接口校验 8 位,明天另一个接口校验 16 位,排查起来很烦。
第三个,用枚举收住魔法字符串。
from enum import Enum
from pydantic import BaseModelclassRefundType(str, Enum):
user_apply = "user_apply"
system_cancel = "system_cancel"
risk_reject = "risk_reject"
classRefundReq(BaseModel):
order_id: str
refund_type: RefundType
这种字段最怕前端随手传个 manual,后端某个分支没覆盖,最后落库变成脏状态。枚举虽然啰嗦一点,但它能把错误挡在入口。
第四个,跨字段校验别写在接口里。
比如提现接口,银行卡提现必须有 bank_card_no,余额提现不需要。
from pydantic import BaseModel, model_validatorclassWithdrawReq(BaseModel):
user_id: str
method: str
amount: int
bank_card_no: str | None = None
@model_validator(mode="after")
defcheck_bank_card(self):
if self.method == "bank"andnot self.bank_card_no:
raise ValueError("bank_card_no required when method is bank")
if self.amount <= 0:
raise ValueError("amount must be greater than 0")
return self
这个判断写在接口函数里也能跑,但我不太喜欢。请求模型都没干净,业务代码就别往下走。
第五个,请求体可以分层,不要一坨塞到底。
from pydantic import BaseModel, FieldclassAddress(BaseModel):
province: str
city: str
detail: str = Field(min_length=5, max_length=128)
classCreateOrderReq(BaseModel):
user_id: str
sku_id: str
count: int = Field(gt=0, le=99)
address: Address
嵌套模型的好处不是“看着高级”,而是错误能定位得更准。
前端传错时,返回一般能看到类似:
{
"loc": ["body", "address", "detail"],
"msg": "String should have at least 5 characters"
}
这比你手写一句“参数错误”强多了。
第六个,列表参数必须限制长度。
from pydantic import BaseModel, FieldclassBatchSkuReq(BaseModel):
sku_ids: list[str] = Field(min_length=1, max_length=100)
批量接口不限制数量,迟早有人一次传几千个。然后你还在查数据库慢 SQL,其实问题从请求入口就该拦掉。
第七个,默认值别乱给。
from pydantic import BaseModel, Field
from datetime import datetimeclassAuditReq(BaseModel):
operator: str
reason: str | None = None
created_at: datetime = Field(default_factory=datetime.now)
这里注意 default_factory,别写成:
created_at: datetime = datetime.now()
这个坑不新鲜,但现场真见过。服务启动时算一次,后面每个请求拿到的默认时间都一样,看日志的时候很容易把人带偏。
第八个,响应模型也要校验。
很多人只管请求,不管返回。接口返回字段越多,越容易把内部字段带出去。
from pydantic import BaseModelclassUserResp(BaseModel):
user_id: str
nickname: str
vip_level: int
@app.get("/users/{user_id}", response_model=UserResp)
defget_user(user_id: str):
row = query_user_from_db(user_id)
return row
数据库里可能还有 phone、id_card、password_hash。你不设 response_model,哪天有人直接 return row,就挺刺激。
第九个,别怕自定义错误,但别写得太花。
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponseapp = FastAPI()
@app.exception_handler(RequestValidationError)
asyncdefvalidation_error_handler(request: Request, exc: RequestValidationError):
first = exc.errors()[0]
return JSONResponse(
status_code=422,
content={
"code": "PARAM_INVALID",
"field": ".".join(str(x) for x in first.get("loc", [])),
"message": first.get("msg")
}
)
统一错误格式这事很重要。否则前端一会儿解析 detail,一会儿解析 msg,接口多了以后,全靠猜。
第十个,模型别复用过头。
创建用户、更新用户、后台审核用户,看起来都是用户字段,但模型最好拆开。
classUserCreateReq(BaseModel):
nickname: str = Field(min_length=2, max_length=20)
phone: strclassUserUpdateReq(BaseModel):
nickname: str | None = Field(default=None, min_length=2, max_length=20)
avatar: str | None = None
classUserAuditReq(BaseModel):
user_id: str
passed: bool
remark: str | None = None
省一个模型,后面会补十个判断。尤其是更新接口,字段通常都是可选的,和创建接口硬共用,最后不是校验太松,就是校验太死。
FastAPI 的请求验证,别只理解成“参数类型校验”。它更像接口门口的安检。
能在 Pydantic 层挡掉的,就别放进 service;能用模型表达清楚的,就别靠注释;能在入口统一处理的,就别让每个接口各写一套。
面试聊到这里,基本就能看出来一个人有没有真写过接口。只会写 name: str、age: int 的,多半还停在 demo 阶段。线上请求没那么干净,Pydantic 用得好,能少看不少脏日志。