Path、Query、Body,FastAPI 参数系统别再混了

很多人学 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

这里的 pagepageSize 是查询参数,因为它们在问号后面。

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 会做三件事:

  1. /book/3 里取出 3
  2. 根据 id: int 把它转成整数
  3. 根据 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

categoryIdpagepageSize 都是查询参数。

新闻详情:

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_classresponse_model 到底有什么区别。

参考资料