Python技术迷

FastAPI请求验证:10 个 Pydantic进阶技巧

FastAPI 接口一上线,最先炸的往往不是业务代码,是请求参数。

日志里一串 422 Unprocessable Entity,前端说“我传了”,后端说“我没收到”,最后翻出来一看,不是字段少了,就是类型被悄悄转了。

Pydantic 这东西,用浅了就是 BaseModel 加几个字段;用深一点,它其实是 FastAPI 请求入口的第一道闸门。面试问它,不是想听你背概念,是想看你知不知道线上那些脏请求怎么挡。

下面这 10 个点,我平时写 FastAPI 接口基本都会用到。

第一个,别什么都让 Pydantic 自动转。

很多人喜欢这样写:

from pydantic import BaseModel

classPayReq(BaseModel):
    order_id: int
    amount: float

看着没问题,但前端传 "1001",Pydantic 也给你转成 1001。有些场景能忍,有些场景不能忍。

比如订单号、手机号、外部流水号,这些字段我一般不让它乱转。

from pydantic import BaseModel, StrictStr, condecimal

classPayReq(BaseModel):
    order_id: StrictStr
    amount: condecimal(gt=0, max_digits=10, decimal_places=2)

金额也别用 float,这地方我第一眼就不信。支付、账单、积分兑换,能用 Decimal 就别用浮点数。

第二个,字段限制要写在模型里,别散在接口里。

from pydantic import BaseModel, Field

classCouponReq(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 BaseModel

classRefundType(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_validator

classWithdrawReq(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, Field

classAddress(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, Field

classBatchSkuReq(BaseModel):
    sku_ids: list[str] = Field(min_length=1, max_length=100)

批量接口不限制数量,迟早有人一次传几千个。然后你还在查数据库慢 SQL,其实问题从请求入口就该拦掉。

第七个,默认值别乱给。

from pydantic import BaseModel, Field
from datetime import datetime

classAuditReq(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 BaseModel

classUserResp(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 JSONResponse

app = 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: str

classUserUpdateReq(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 用得好,能少看不少脏日志。