很多人学 FastAPI 的第一个坎,是参数。
看代码时感觉都差不多:
python
async def get_book(id: int):
...
python
async def get_news(skip: int = 0, limit: int = 10):
...
python
async def register(user: User):
...
但请求一发出去,有的参数在 URL 路径里,有的参数在问号后面,有的参数在 JSON 请求体里。
这篇就把 FastAPI 里最常用的三类参数一次讲清楚:
- Path,路径参数
- Query,查询参数
- Body,请求体参数
你只要搞懂一句话就行:
路径参数负责找谁,查询参数负责怎么筛,请求体负责提交什么内容。
本篇准备
这一篇继续使用 FastAPI 基础环境:
bash
pip install fastapi uvicorn
FastAPI 会通过 Pydantic 做参数解析和校验。只要安装了 fastapi,通常就已经带上了 Pydantic;本文示例按 Pydantic v2 写法理解。
1. 参数到底放在哪里
先看一张图。
#mermaid-svg-2TE5Wf8MSL0mgA43{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2TE5Wf8MSL0mgA43 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2TE5Wf8MSL0mgA43 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2TE5Wf8MSL0mgA43 .error-icon{fill:#552222;}#mermaid-svg-2TE5Wf8MSL0mgA43 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2TE5Wf8MSL0mgA43 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2TE5Wf8MSL0mgA43 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2TE5Wf8MSL0mgA43 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2TE5Wf8MSL0mgA43 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2TE5Wf8MSL0mgA43 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2TE5Wf8MSL0mgA43 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2TE5Wf8MSL0mgA43 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2TE5Wf8MSL0mgA43 .marker.cross{stroke:#333333;}#mermaid-svg-2TE5Wf8MSL0mgA43 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2TE5Wf8MSL0mgA43 p{margin:0;}#mermaid-svg-2TE5Wf8MSL0mgA43 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2TE5Wf8MSL0mgA43 .cluster-label text{fill:#333;}#mermaid-svg-2TE5Wf8MSL0mgA43 .cluster-label span{color:#333;}#mermaid-svg-2TE5Wf8MSL0mgA43 .cluster-label span p{background-color:transparent;}#mermaid-svg-2TE5Wf8MSL0mgA43 .label text,#mermaid-svg-2TE5Wf8MSL0mgA43 span{fill:#333;color:#333;}#mermaid-svg-2TE5Wf8MSL0mgA43 .node rect,#mermaid-svg-2TE5Wf8MSL0mgA43 .node circle,#mermaid-svg-2TE5Wf8MSL0mgA43 .node ellipse,#mermaid-svg-2TE5Wf8MSL0mgA43 .node polygon,#mermaid-svg-2TE5Wf8MSL0mgA43 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2TE5Wf8MSL0mgA43 .rough-node .label text,#mermaid-svg-2TE5Wf8MSL0mgA43 .node .label text,#mermaid-svg-2TE5Wf8MSL0mgA43 .image-shape .label,#mermaid-svg-2TE5Wf8MSL0mgA43 .icon-shape .label{text-anchor:middle;}#mermaid-svg-2TE5Wf8MSL0mgA43 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2TE5Wf8MSL0mgA43 .rough-node .label,#mermaid-svg-2TE5Wf8MSL0mgA43 .node .label,#mermaid-svg-2TE5Wf8MSL0mgA43 .image-shape .label,#mermaid-svg-2TE5Wf8MSL0mgA43 .icon-shape .label{text-align:center;}#mermaid-svg-2TE5Wf8MSL0mgA43 .node.clickable{cursor:pointer;}#mermaid-svg-2TE5Wf8MSL0mgA43 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2TE5Wf8MSL0mgA43 .arrowheadPath{fill:#333333;}#mermaid-svg-2TE5Wf8MSL0mgA43 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2TE5Wf8MSL0mgA43 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2TE5Wf8MSL0mgA43 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2TE5Wf8MSL0mgA43 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2TE5Wf8MSL0mgA43 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2TE5Wf8MSL0mgA43 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2TE5Wf8MSL0mgA43 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2TE5Wf8MSL0mgA43 .cluster text{fill:#333;}#mermaid-svg-2TE5Wf8MSL0mgA43 .cluster span{color:#333;}#mermaid-svg-2TE5Wf8MSL0mgA43 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2TE5Wf8MSL0mgA43 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2TE5Wf8MSL0mgA43 rect.text{fill:none;stroke-width:0;}#mermaid-svg-2TE5Wf8MSL0mgA43 .icon-shape,#mermaid-svg-2TE5Wf8MSL0mgA43 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2TE5Wf8MSL0mgA43 .icon-shape p,#mermaid-svg-2TE5Wf8MSL0mgA43 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2TE5Wf8MSL0mgA43 .icon-shape .label rect,#mermaid-svg-2TE5Wf8MSL0mgA43 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2TE5Wf8MSL0mgA43 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2TE5Wf8MSL0mgA43 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2TE5Wf8MSL0mgA43 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} URL 路径中
? 后面
HTTP Body 中
客户端请求
数据在哪里
Path 路径参数
Query 查询参数
Body 请求体
定位资源,例如 /book/3
筛选分页,例如 ?page=1&pageSize=10
创建或更新资源,例如 POST JSON
对应到真实请求:
http
GET /book/3
这里的 3 是路径参数,因为它是 URL 路径的一部分。
http
GET /news/list?page=1&pageSize=10
这里的 page 和 pageSize 是查询参数,因为它们在问号后面。
http
POST /user/register
Content-Type: application/json
{
"username": "tom",
"password": "123456"
}
这里的 JSON 是请求体参数。
2. Path,路径参数用来定位资源
路径参数写在 URL 路径里,最典型的形式是:
python
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/book/{id}")
async def get_book(
id: int = Path(..., gt=0, lt=101, description="书籍 id,范围 1-100")
):
return {"id": id, "title": f"第 {id} 本书"}
访问:
http
GET /book/3
FastAPI 会做三件事:
- 从
/book/3里取出3 - 根据
id: int把它转成整数 - 根据
Path(..., gt=0, lt=101)校验范围
如果你访问:
http
GET /book/abc
它会返回 422,因为 abc 不能转成整数。
如果你访问:
http
GET /book/0
也会返回 422,因为 gt=0 要求大于 0。
Path(...) 常用参数如下:
| 参数 | 作用 |
|---|---|
... |
必填 |
gt |
大于 |
ge |
大于等于 |
lt |
小于 |
le |
小于等于 |
min_length |
字符串最小长度 |
max_length |
字符串最大长度 |
description |
文档说明 |
路径参数的判断规则很简单:
只要路径里写了 {id},函数参数里就必须有同名参数 id。
3. Query,查询参数用来筛选和分页
查询参数出现在 URL 的问号后面。
课程里的新闻列表就是典型例子:
python
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/news/list")
async def get_news_list(
skip: int = Query(0, ge=0, description="跳过多少条"),
limit: int = Query(10, ge=1, le=100, description="最多返回多少条")
):
return {"skip": skip, "limit": limit}
访问:
http
GET /news/list?skip=20&limit=10
返回:
json
{
"skip": 20,
"limit": 10
}
如果请求里不传:
http
GET /news/list
就使用默认值:
json
{
"skip": 0,
"limit": 10
}
这就是 Query 和 Path 的一个重要区别。
路径参数通常是必填的,因为没有它就不知道你要操作哪个资源。
查询参数通常可以有默认值,因为它更多是在表达筛选、排序、分页。
4. Body,请求体用来提交复杂数据
新增用户、新增新闻、更新资料,这些数据字段多,结构也更复杂,不适合塞进 URL。
这时就用请求体。
FastAPI 通常用 Pydantic 模型定义请求体:
python
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class UserRegisterRequest(BaseModel):
username: str = Field(min_length=2, max_length=20, description="用户名")
password: str = Field(min_length=6, max_length=50, description="密码")
@app.post("/user/register")
async def register(user: UserRegisterRequest):
return {
"username": user.username,
"message": "注册成功"
}
请求:
http
POST /user/register
Content-Type: application/json
{
"username": "tom",
"password": "123456"
}
FastAPI 会把 JSON 转成 UserRegisterRequest 对象。
如果缺字段、字段类型不对、长度不符合要求,就直接返回 422。
5. 422 错误怎么读
很多新手看到 422 会慌。
其实 422 是 FastAPI 在告诉你,请求参数不符合接口声明。
例如:
http
GET /book/abc
简化后的错误可能类似这样:
json
{
"detail": [
{
"loc": ["path", "id"],
"msg": "Input should be a valid integer",
"type": "int_parsing"
}
]
}
重点看 loc。
text
["path", "id"]
表示错误发生在路径参数 id。
如果是:
text
["query", "page"]
表示查询参数 page 有问题。
如果是:
text
["body", "password"]
表示请求体里的 password 字段有问题。
#mermaid-svg-t952o4WG6pfY8stf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-t952o4WG6pfY8stf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-t952o4WG6pfY8stf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-t952o4WG6pfY8stf .error-icon{fill:#552222;}#mermaid-svg-t952o4WG6pfY8stf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-t952o4WG6pfY8stf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-t952o4WG6pfY8stf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-t952o4WG6pfY8stf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-t952o4WG6pfY8stf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-t952o4WG6pfY8stf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-t952o4WG6pfY8stf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-t952o4WG6pfY8stf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-t952o4WG6pfY8stf .marker.cross{stroke:#333333;}#mermaid-svg-t952o4WG6pfY8stf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-t952o4WG6pfY8stf p{margin:0;}#mermaid-svg-t952o4WG6pfY8stf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-t952o4WG6pfY8stf .cluster-label text{fill:#333;}#mermaid-svg-t952o4WG6pfY8stf .cluster-label span{color:#333;}#mermaid-svg-t952o4WG6pfY8stf .cluster-label span p{background-color:transparent;}#mermaid-svg-t952o4WG6pfY8stf .label text,#mermaid-svg-t952o4WG6pfY8stf span{fill:#333;color:#333;}#mermaid-svg-t952o4WG6pfY8stf .node rect,#mermaid-svg-t952o4WG6pfY8stf .node circle,#mermaid-svg-t952o4WG6pfY8stf .node ellipse,#mermaid-svg-t952o4WG6pfY8stf .node polygon,#mermaid-svg-t952o4WG6pfY8stf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-t952o4WG6pfY8stf .rough-node .label text,#mermaid-svg-t952o4WG6pfY8stf .node .label text,#mermaid-svg-t952o4WG6pfY8stf .image-shape .label,#mermaid-svg-t952o4WG6pfY8stf .icon-shape .label{text-anchor:middle;}#mermaid-svg-t952o4WG6pfY8stf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-t952o4WG6pfY8stf .rough-node .label,#mermaid-svg-t952o4WG6pfY8stf .node .label,#mermaid-svg-t952o4WG6pfY8stf .image-shape .label,#mermaid-svg-t952o4WG6pfY8stf .icon-shape .label{text-align:center;}#mermaid-svg-t952o4WG6pfY8stf .node.clickable{cursor:pointer;}#mermaid-svg-t952o4WG6pfY8stf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-t952o4WG6pfY8stf .arrowheadPath{fill:#333333;}#mermaid-svg-t952o4WG6pfY8stf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-t952o4WG6pfY8stf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-t952o4WG6pfY8stf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-t952o4WG6pfY8stf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-t952o4WG6pfY8stf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-t952o4WG6pfY8stf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-t952o4WG6pfY8stf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-t952o4WG6pfY8stf .cluster text{fill:#333;}#mermaid-svg-t952o4WG6pfY8stf .cluster span{color:#333;}#mermaid-svg-t952o4WG6pfY8stf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-t952o4WG6pfY8stf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-t952o4WG6pfY8stf rect.text{fill:none;stroke-width:0;}#mermaid-svg-t952o4WG6pfY8stf .icon-shape,#mermaid-svg-t952o4WG6pfY8stf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-t952o4WG6pfY8stf .icon-shape p,#mermaid-svg-t952o4WG6pfY8stf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-t952o4WG6pfY8stf .icon-shape .label rect,#mermaid-svg-t952o4WG6pfY8stf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-t952o4WG6pfY8stf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-t952o4WG6pfY8stf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-t952o4WG6pfY8stf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 422 响应
看 loc
path
query
body
检查 URL 路径
检查问号后的参数
检查 JSON 请求体
6. Path、Query、Body 怎么选
实际写接口时,可以按这张表判断:
| 需求 | 推荐位置 | 示例 |
|---|---|---|
| 指定某一个资源 | Path | /news/3 |
| 分页、搜索、筛选 | Query | /news/list?page=1&pageSize=10 |
| 创建资源 | Body | POST /news + JSON |
| 更新资源内容 | Body + Path | PUT /news/3 + JSON |
| 身份认证信息 | Header | Authorization: Bearer xxx |
尤其要注意更新接口。
路径参数负责找谁,请求体负责改成什么。
python
@app.put("/book/{book_id}")
async def update_book(book_id: int, data: BookUpdateRequest):
...
book_id 表示要更新哪本书。
data 表示新内容。
这两个概念不要混在一起。
7. 在 AI 掘金头条项目里的对应关系
项目里的接口基本都能套进这套规则。
新闻列表:
http
GET /api/news/list?categoryId=1&page=1&pageSize=10
categoryId、page、pageSize 都是查询参数。
新闻详情:
http
GET /api/news/detail?id=3
课程项目里把 id 放在 Query 中。真实 REST 风格也可以设计成:
http
GET /api/news/3
收藏新闻:
http
POST /api/favorite/add
Content-Type: application/json
{
"newsId": 3
}
这里 newsId 放在请求体,因为它是一个动作提交。
删除浏览历史:
http
DELETE /api/history/delete/{history_id}
这里 history_id 是路径参数,因为它定位的是一条历史记录。
8. 小结
FastAPI 参数系统其实不复杂。
记住这三句话:
text
Path,路径里,负责找谁
Query,问号后,负责筛选和分页
Body,请求体里,负责提交复杂内容
再配合 Pydantic,你就能让 FastAPI 自动完成:
- 类型转换
- 参数校验
- 错误响应
- 接口文档生成
这就是 FastAPI 写 API 很舒服的原因。
下一篇我们继续往后看,函数返回的数据又是怎么变成响应的,response_class 和 response_model 到底有什么区别。