别再手动校验 JSON:用 Pydantic 守住 FastAPI 请求边界
摘要:从创建任务的 JSON 请求出发,用 Pydantic v2 处理必填字段、默认值、范围限制和空白标题,让错误数据在进入业务逻辑前得到清晰的 422 响应。

图:请求数据先经过模型、必填项和范围校验,合法数据才会进入业务逻辑。
创建任务时,客户端通常会提交一段 JSON。手动调用 request.json()、逐个检查字段、转换数字当然能做,但代码很快会被边界判断占满。
FastAPI 与 Pydantic 的组合提供了更稳定的入口:先声明“合法任务长什么样”,框架负责解析、转换、校验和生成文档。下面从一条 POST 请求开始,把这道边界完整搭起来。
先提交一条任务,观察输入和输出
fastapi dev examples/ch03_request_body/main.py
创建任务:
curl -i -X POST http://127.0.0.1:8000/tasks \
-H "Content-Type: application/json" \
-d '{"title":" 学习 Pydantic ","priority":5,"tags":["fastapi"]}'
预期状态码为 201,返回的标题已经去掉首尾空格,未传的 description 为 null。
先确认示例使用的是 Pydantic v2
cd 04-fastapi-beginner
source .venv/bin/activate
python -c "import pydantic; print(pydantic.__version__)"
本系列使用 Pydantic v2,序列化使用 model_dump(),不沿用旧教程中的 .dict() 主写法。
先声明一条合法任务的形状
定义请求模型
from pydantic import BaseModel, Field
class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=100)
description: str | None = Field(default=None, max_length=500)
priority: int = Field(default=3, ge=1, le=5)
tags: list[str] = Field(default_factory=list, max_length=5)
title 没有默认值,所以必填。description 可省略,priority 省略时为 3。列表使用 default_factory=list,避免多个对象共享同一个可变默认值。
拒绝纯空格标题
@field_validator("title")
@classmethod
def title_must_not_be_blank(cls, value: str) -> str:
title = value.strip()
if not title:
raise ValueError("title cannot be blank")
return title
长度校验无法区分一个字符和一个空格,自定义校验器先 strip(),再判断清理后的结果。
接收模型对象
@app.post("/tasks", status_code=status.HTTP_201_CREATED)
def create_task(payload: TaskCreate) -> dict[str, object]:
return {"id": 1, **payload.model_dump()}
函数执行时,payload 已经是 TaskCreate 对象,不需要手动读取 JSON、判断字段或转换整数。

图:请求模型先声明合法数据的形状,再让所有请求统一经过这道边界。
故意提交三种错误 JSON
合法请求:
curl -i -X POST http://127.0.0.1:8000/tasks \
-H "Content-Type: application/json" \
-d '{"title":"写教程"}'
依次制造错误:
curl -i -X POST http://127.0.0.1:8000/tasks -H "Content-Type: application/json" -d '{}'
curl -i -X POST http://127.0.0.1:8000/tasks -H "Content-Type: application/json" -d '{"title":"任务","priority":6}'
curl -i -X POST http://127.0.0.1:8000/tasks -H "Content-Type: application/json" -d '{"title":" "}'
预期三条均为 422。第一条错误位置为 body -> title,第二条说明 priority 不得大于 5,第三条来自自定义校验器。
python -m pytest tests/test_ch03.py -q
从 JSON 到 TaskCreate 经历了什么
一次 POST 请求经历:JSON 字节 → JSON 对象 → Pydantic 校验和转换 → TaskCreate 实例 → 业务函数。任一步校验失败,FastAPI 都不会调用 create_task。
模型同时服务于四个场景:运行时验证、编辑器类型提示、OpenAPI Schema、Swagger 表单。修改字段约束后刷新 /docs,界面中的模型定义也会变化。
不要用一个模型承载所有阶段。创建时不应该让客户端传 id,读取时却必须返回 id。下一篇会拆分 TaskCreate 与 TaskRead。

图:JSON 请求体先被解析,再执行模型校验,最后才成为路由可以使用的对象。
请求体最容易错在这五处
- 忘记
Content-Type: application/json:服务无法按预期解析请求体。 - JSON 使用单引号包字段:JSON 内部必须是双引号;shell 外层可用单引号。
- 写成
description: str | None却不给默认值:字段仍然必填,只是允许值为 null。 - 使用
tags=[]作为可变默认值:优先使用default_factory=list。 - 只写
min_length=1:纯空格仍能通过,需要清理或业务校验。
把模型再推近真实业务一步
- 增加
estimated_minutes,限制为 1~1440,默认 30。 - 限制每个 tag 的长度为 1~20;提示:定义单独的受约束字符串类型。
- 提交字符串形式的 priority,例如
"5",观察是否被转换。 - 为超过 5 个 tags 的请求增加测试。
- 排错:故意删除请求头,比较 curl 响应和服务器日志。
最后,用测试固定这次改动
手动请求通过后,运行与本文对应的自动化测试:
python -m pytest tests/test_ch03.py -q
python tests/validate_course.py
写在最后
Pydantic 把散乱的 JSON 变成有类型、有约束的应用边界。下一篇将控制另一个方向:服务器允许向客户端返回哪些字段,以及如何用 201、404 等状态码表达结果。