写API接口写多了你会发现一个挺烦人的现象,查询参数越攒越多,函数签名越写越长,一个接口十几个参数堆在括号里,看着就让人头皮发麻。FastAPI在0.115.0版本给出了一个挺优雅的解法,让你能用Pydantic模型把这些参数打包起来,既省心又好维护。这篇文章就带你把这套机制彻底摸透,从最朴素的写法讲起,一路讲到它能帮你避开哪些真实踩过的坑。
为什么需要查询参数模型
先回想一下没有这个功能之前大家是怎么写的。最简单粗暴的方式就是把每个查询参数直接写成函数参数,类似这样。
python
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def search(limit: int | None = 10, skip: int | None = 1, filter: str | None = None):
return {"limit": limit, "skip": skip, "filter": filter}
这样写没什么问题,接口一调就通。可一旦你发现好几个接口都要用到同一组分页参数,麻烦就来了,你要么复制粘贴一遍参数列表,要么想办法抽出去复用。早年间社区里比较流行的做法是写一个依赖函数,把参数打包成字典或者Pydantic模型再返回,通过Depends注入进去。这套方案能用,但写起来总归绕了一圈,参数声明和参数校验分散在函数签名和依赖函数两个地方,读代码的人得来回跳着看。
FastAPI团队后来干脆把这件事做成了一等公民的功能,直接支持把整组查询参数声明成一个Pydantic模型,FastAPI会自动帮你完成校验、生成文档、处理默认值这些活。这也是为什么Rafael Marques在他那篇文章里说,这是当时FastAPI社区里呼声最高的功能之一,等了好一阵子终于等来了。
下面这张图能帮你直观感受一下前后两种写法的差别。
基本用法:声明一个Pydantic模型当查询参数
核心思路很简单,先定义一个继承自BaseModel的类,把想要的查询参数都写成类的字段,然后在路径操作函数里把这个模型类型标注为Query()。官方文档给出的示例是这样的。
python
from typing import Annotated, Literal
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI()
class FilterParams(BaseModel):
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items/")
async def read_items(filter_query: Annotated[FilterParams, Query()]):
return filter_query
这段代码里藏着不少值得拆开细讲的小知识点。
字段校验直接写进模型 ,比如limit用Field(100, gt=0, le=100)意味着默认值100,同时要求这个数必须大于0且小于等于100,超出范围FastAPI会自动返回422错误,连你自己写校验逻辑的力气都省了。
Literal类型能限定枚举值 ,order_by只能是created_at或者updated_at这两个字符串之一,传别的值同样会报错,这在做排序、筛选这类有限选项的参数时特别好用。
列表类型的参数天然支持多值传参 ,tags: list[str] = []意味着客户端可以在URL里写?tags=a&tags=b&tags=c,FastAPI会自动把它们收集成一个列表。
用Annotated[FilterParams, Query()]这种写法是官方推荐的方式,把类型信息和查询参数的元信息分开标注,可读性比老式写法要清晰不少。
自动生成的接口文档长什么样
模型里声明的这些校验规则和默认值,不只是在运行时生效,还会被FastAPI自动同步进OpenAPI文档里,也就是你打开Swagger UI能看到的那个交互式界面。每个字段的类型、取值范围、默认值都会清清楚楚地展示出来,前端同学或者调用你接口的第三方开发者不用翻代码就能明白怎么传参。这其实是FastAPI这个框架一直以来的核心卖点,代码即文档,你写业务逻辑的同时文档就顺带生成好了。
禁止额外的查询参数:一个真实踩过的坑
这一块内容特别有意思,因为它源自一个真实的线上问题反馈。早在2021年,GitHub上有个开发者stevenj提了一个问题,他们团队的接口原本设计成用id这个字段名接收参数。
python
@endpoint.get("/list")
async def device_list(device_id: Optional[str] = Query(None, alias="id")):
return List.device(device_id)
结果有个客户端调用方一直传的是device_id而不是id,接口收不到正确的值,就默默返回了默认结果。问题是接口表面看起来完全正常,200状态码,正常的JSON返回,调用方压根意识不到自己传错了参数名,排查起来相当折磨人。
这类问题的根源在于,默认情况下查询参数里多传一些没用到的字段,FastAPI是不会报错的,它就当没看见直接忽略掉。stevenj当时就在问,有没有办法让FastAPI在遇到未知参数时主动报错,而不是悄悄吞掉。
社区给出的方案挺巧妙,利用Pydantic模型自带的extra配置项。
python
from pydantic import BaseModel, Field, Extra
from fastapi import FastAPI, Depends
class MyModel(BaseModel):
device_id: str = Field(alias="id")
class Config:
extra = Extra.forbid
app = FastAPI()
@app.get("/list")
async def device_list(req_model: MyModel = Depends()):
return req_model.device_id
把extra设成forbid之后,只要请求里出现了模型没定义过的字段,FastAPI就会直接返回422错误,报错信息里还会清楚指出哪个字段是多余的。按现在Pydantic v2的写法,这行配置一般会改成下面这样,效果是一致的。
python
from pydantic import BaseModel, Field
class FilterParams(BaseModel):
model_config = {"extra": "forbid"}
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
官方文档也把这个特性收录了进去,专门起了个小节叫查看文档和禁止额外的查询参数,可见这不是什么边角料功能,而是实打实解决过真实痛点的设计。
下面用一张流程图梳理一下开启这个配置前后,请求的处理路径有什么不同。
这张图其实挺能说明问题,同样是多传了一个参数,配置前后的表现天差地别。前者是那种表面正常、暗地里出问题的隐患,后者则是把问题第一时间暴露出来,让调用方立刻知道自己哪里传错了。对接口的健壮性来说,后者显然更让人安心。
几种写法的对比
走到这里,可以把三种典型写法放在一起比一比,看看各自的适用场景。
| 写法 | 代码复杂度 | 参数复用性 | 额外参数检测 | 适用场景 |
|---|---|---|---|---|
| 逐个声明参数 | 低,写起来最直接 | 差,改一处要改多处 | 天然忽略多余参数 | 参数少、只有单个接口用 |
| Depends依赖函数 | 中,需要额外写一层函数 | 好,可跨接口复用 | 需手动处理 | Pydantic模型功能出现之前的过渡方案 |
| Pydantic查询参数模型 | 低,声明式风格 | 好,模型可直接跨接口复用 | 配置extra forbid即可开启 | 参数多、需要严格校验的场景 |
从这张表也能看出,Pydantic模型这条路径基本是集大成者,把声明简洁、参数复用、严格校验这几个诉求一次性都照顾到了。
写在最后
FastAPI这个查询参数模型的功能,表面上看只是省了几行代码,往深了想其实体现的是一种挺重要的工程思路,让相关联的东西聚在一起管理,而不是零零散散地铺在函数签名里。分页参数、筛选条件这类经常成组出现的字段,用一个模型统一收纳,既方便复用又方便加校验规则,出了问题也好排查。
extra forbid这个小配置尤其值得单独记一下,它解决的是那种最讨厌的隐性bug,接口看着一切正常,实际上调用方早就传错了参数名却毫不知情。如果你的项目里对外暴露的接口比较多,尤其是给第三方调用的场景,建议养成习惯,凡是用Pydantic模型接查询参数的地方,都顺手加上这行配置,能帮你省下不少排查线上问题的时间。
参考资料
查询参数模型 FastAPI中文文档 fastapi.org.cn/tutorial/qu...
Query Parameter Models FastAPI Official Documentation fastapi.tiangolo.com/tutorial/qu...
Forbid Extra Query Parameters GitHub Discussion github.com/fastapi/fas...
FastAPI How to use Pydantic to declare Query Parameters by Rafael de Oliveira Marques dev.to/ceb10n/fast...