FastAPI 如何接收 URL 参数?路径、查询与 422 一次讲清
摘要:用任务详情和筛选接口演示路径参数、查询参数、分页约束与类型转换,并通过几次故意失败的请求读懂 FastAPI 返回的 422。

图:路径参数定位一条资源,查询参数负责筛选,错误类型会在进入业务逻辑前被拦截。
固定的 /health 只能回答一个固定问题。真实 API 还要处理“查看 7 号任务”“只看未完成任务”“从第 20 条开始取 10 条”这样的输入。
这些值都写在 URL 上,却不应该以未经检查的字符串直接进入业务代码。下面用两个接口把路径参数、查询参数和参数校验串起来,同时解释为什么 /tasks/abc 会在函数执行前就被拒绝。
先看两个真正带参数的请求
启动示例:
fastapi dev examples/ch02_path_query/main.py
请求指定任务:
curl http://127.0.0.1:8000/tasks/7
预期响应:
{"task_id":7,"message":"正在查看任务 7"}
筛选未完成任务并限制一条:
curl "http://127.0.0.1:8000/tasks?completed=false&limit=1"
先准备三条可以筛选的任务数据
从仓库根目录进入项目并激活环境:
cd 04-fastapi-beginner
source .venv/bin/activate
python -c "import fastapi; print(fastapi.__version__)"
打开 示例代码。本文使用三条固定任务,暂时不处理创建和数据库。
让 URL 参数先通过类型和范围校验
声明路径参数
from typing import Annotated
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/tasks/{task_id}")
def read_task(
task_id: Annotated[int, Path(ge=1, description="任务 ID")],
):
return {"task_id": task_id, "message": f"正在查看任务 {task_id}"}
{task_id} 与函数参数同名,因此 FastAPI 知道它来自路径。int 要求把 URL 文本转换成整数,ge=1 要求它不小于 1。
声明查询参数
@app.get("/tasks")
def list_tasks(
completed: bool | None = None,
q: Annotated[str | None, Query(max_length=50)] = None,
offset: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=100)] = 20,
):
...
没有出现在路径里的简单类型参数默认来自查询字符串。默认值为 None 表示可以不传;limit=20 表示不传时采用 20。
实现筛选和分页
result = TASKS
if completed is not None:
result = [task for task in result if task["completed"] is completed]
if q:
keyword = q.casefold()
result = [task for task in result if keyword in str(task["title"]).casefold()]
return result[offset : offset + limit]
分页使用列表切片。真实数据库会在 SQL 查询中完成分页,具体实现可以继续阅读 把内存数据换成 SQLite。
用正常值和错误值各请求一次
正常路径:
curl -i http://127.0.0.1:8000/tasks/2
curl -i "http://127.0.0.1:8000/tasks?q=查询"
curl -i "http://127.0.0.1:8000/tasks?offset=1&limit=1"
主动测试边界:
curl -i http://127.0.0.1:8000/tasks/0
curl -i http://127.0.0.1:8000/tasks/abc
curl -i "http://127.0.0.1:8000/tasks?limit=101"
后三条预期都是 422 Unprocessable Content。服务没有崩溃,而是在调用业务函数之前拒绝了非法输入。
自动化验证:
python -m pytest tests/test_ch02.py -q
FastAPI 如何判断参数来自哪里
FastAPI 判断参数来源的基本规则:
| 声明方式 | 参数来源 | 示例 |
|---|---|---|
| 名称出现在路径模板 | 路径 | /tasks/{task_id} |
| 简单类型且不在路径 | 查询字符串 | ?limit=20 |
| Pydantic 模型 | 请求体 | 下一篇的 JSON |
URL 中所有内容最初都是文本。类型注解不只是编辑器提示,FastAPI 会据此完成转换、校验和文档生成。completed=false 最终进入函数时已经是 Python 的 False。
422 表示请求格式能被服务器理解,但参数值不符合接口契约。它和以后遇到的 404 不同:404 通常表示参数合法,但资源不存在。

图:参数写在不同位置,FastAPI 就会从路径、查询字符串或请求体中读取。
422 不等于服务崩了
- 把 URL 写成
/tasks/{7}:花括号只出现在路由定义,实际请求应为/tasks/7。 - 忘记给可选参数默认值:
str | None描述类型,= None才让参数可省略。 - 在 shell 中不加引号:带
&的 URL 必须整体用引号包住,否则 shell 会拆分命令。 - 用
if completed:判断过滤:当值为False时不会进入分支,应使用is not None。 - 收到 422 就修改业务函数:先阅读响应里的
detail、loc、msg,函数可能根本没有执行。

图:类型转换和范围检查发生在路由执行之前,校验失败会直接形成错误响应。
筛选接口还可以继续加什么
- 基础:增加
limit默认值为 2,确认不传参数只返回两条。 - 改造:增加
sort=asc|desc,按任务 ID 排序;非法值应返回 422。 - 排错:把
offset传成-1,从 422 响应中找到错误位置和约束。 - 测试:为
q=查询增加一个自动化测试。 - 思考:
/tasks/999为什么现在返回 200?资源不存在应该在哪一章解决?
最后,用测试固定这次改动
手动请求通过后,运行与本文对应的自动化测试:
python -m pytest tests/test_ch02.py -q
python tests/validate_course.py
写在最后
本文建立了“外部文本先转换和校验,再进入业务函数”的模型。下一篇会把 JSON 请求体转换成 Pydantic 对象,并处理必填字段、默认值、长度和自定义校验。