P08第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

请求与响应校验

请求与响应校验 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

P07 的接口只收查询参数,这一课给「新增笔记」加上真正的写入口:用 Pydantic 模型校验请求体、用 response_model 控制输出结构,让缺字段、超长、非法枚举在门口就被拦下。

问题场景

前端要新增一条学习笔记:POST /notes,body 是 {title, body, tag}。用户手滑提交了空标题、一万字的正文、或者 tag 写成了 notes(复数)。P07 的写法里这些都是裸 dict 直接进函数——校验逻辑要自己写一坨 if。Pydantic 模型把这些 if 声明成了数据。

前端对应物你已经很熟:zod schema + safeParse。Pydantic 在 FastAPI 里扮演的就是这个角色,而且和路由函数长在一起。

核心解释

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import Literal

app = FastAPI()

class NoteIn(BaseModel):                    # 输入模型:决定「什么能进来」
    title: str = Field(min_length=1, max_length=60)
    body: str = Field(min_length=1, max_length=5000)
    tag: Literal["course", "note", "review"] = "note"

class NoteOut(BaseModel):                   # 输出模型:决定「什么能出去」
    id: str
    title: str
    tag: str

NOTES: list[dict] = []

@app.post("/notes", response_model=NoteOut, status_code=201)
def create_note(note: NoteIn):
    if any(n["title"] == note.title for n in NOTES):
        raise HTTPException(status_code=409, detail="同名笔记已存在")
    item = {"id": f"note_{len(NOTES)+1}", **note.model_dump(), "internal": "secret"}
    NOTES.append(item)
    return item                              # response_model 会过滤掉未声明字段

四条机制看清再动手:

  1. 校验在进函数之前。缺 title、正文超 5000 字、tag 不在白名单,FastAPI 直接返回 422,函数体一行都不执行——和 zod 在数据层拦截同理,只是这里连路由层都省了。
  2. 约束写在字段上。Field(min_length=..., max_length=...) 管长度,Literal/Enum 管枚举,ge/le 管数值范围。错误信息里会精确指出哪个字段、什么原因。
  3. response_model 是出口过滤。函数里多塞的 internal 字段不会出现在响应里;出参和入参分开声明,内部字段就永远不会泄漏。
  4. 业务错误仍用 HTTPException。模型管「形状」,重复写入这类业务冲突还是你自己在函数里抛 409——校验框架管不了业务规则。

示例拆解

用真实请求把三个失败场景打一遍:

uvicorn main:app --reload --port 8100

curl -X POST http://127.0.0.1:8100/notes -H "Content-Type: application/json" ^
  -d "{\"title\":\"函数与模块\",\"body\":\"拆函数心得\",\"tag\":\"note\"}"
# 201,返回 {"id":"note_1","title":"函数与模块","tag":"note"} —— internal 被过滤

curl -X POST http://127.0.0.1:8100/notes -H "Content-Type: application/json" ^
  -d "{\"title\":\"\",\"body\":\"x\"}"
# 422,detail 指出 title 至少 1 个字符

curl -X POST http://127.0.0.1:8100/notes -H "Content-Type: application/json" ^
  -d "{\"title\":\"a\",\"body\":\"x\",\"tag\":\"memo\"}"
# 422,tag 不是合法枚举值

预期:三种情况行为各不相同——合法的 201、形状错的 422、业务冲突的 409。422 响应体里 loc(哪个字段)和 msg(什么问题)是成对出现的,前端可以直接映射成表单错误提示。

动手任务

  1. 给 NoteIn 加 minutes: int | None = Field(default=None, ge=0, le=600),表示关联学习时长,用 /docs 验证越界值被拒。
  2. 新增 GET /notes 返回 list[NoteOut],确认 internal 字段在列表里同样被过滤。
  3. 把 tag 从 Literal 改成 Enum 类实现,对比两种写法的错误信息,选一个留下并说明理由。

常见错误

  • 在函数里手写 if 校验:模型已经声明过的约束又写一遍,两边迟早不一致;形状约束全交模型。
  • 入参出参共用一个模型:内部字段一不小心就随响应漏出去;In/Out 模型分开。
  • 把 422 当 bug 修:422 是校验在工作,先看 loc/msg 修正请求,别急着改服务端。
  • Field 范围拍脑袋:max_length 定得太大会拖垮存储层,太小挡掉正常输入;参照真实数据分布定。

检查与复盘

自查清单:

  • 请求体字段都有长度/范围/枚举约束,且能在 /docs 里验证
  • 能说清 422(形状错)和 409(业务冲突)的分工
  • 响应里找不到任何未在 Out 模型声明的字段

参考资料

检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1读代码较难

阅读代码(FastAPI + Pydantic v2,Python 3.11+)。分别用 page=0 与 page=2 请求 GET /courses,各返回什么?

from fastapi import FastAPI, Query

app = FastAPI()

@app.get('/courses')
def list_courses(page: int = Query(default=1, ge=1, le=2)):
    # 假设库里共 2 页数据
    return {'page': page, 'total_pages': 2}
Q2单选中等

场景:删除课程接口发现课程不存在。从 HTTP 语义与前端统一处理的角度,哪种响应方式更合理?

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...