Python 现代开发学习笔记

(四)从类型注解走到 Pydantic 校验

1. 观察模型创建过程

上一章的类型注解可以提示编辑器,却不会自动检查所有输入。Pydantic 的 BaseModel 把“检查输入并构造对象”连成一个实际动作。先运行一个短例:

# 项目文件:demos/body_model.py
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field


class Draft(BaseModel):
    title: str = Field(min_length=1, max_length=100)
    finished: bool = False


app = FastAPI()


@app.post("/books", response_model=Draft, status_code=201)
def add_book(data: Draft):
    # data 已经是模型对象;点号读取经过校验的字段。
    return data


if __name__ == "__main__":
    with TestClient(app) as client:
        for payload in [
            {"title": "Python Web"},
            {"title": "A", "finished": "false"},
            {"title": 12},
        ]:
            response = client.post("/books", json=payload)
            print(response.status_code, response.json())
# TestClient 在本进程内把请求交给 ASGI 应用,不另外监听端口。
uv run python ../demos/body_model.py

第一条只提交书名,得到 201,finished 采用 False。第二条提交字符串 "false",这个宽松模型把它转成布尔值 False。第三条把书名写成数字,得到 422,位置指向 body 中的 title。

这里没有执行 Draft(**data) 的手写路由代码,FastAPI 根据 data: Draft 执行了这一步。模型失败时函数体不执行;但请求的不同字段和依赖由框架一起解析,身份错误与字段错误同时存在时,要根据实际响应定位,不能把整个解析过程想成一条固定的逐项队列。

2. 按书架规则建立输入模型

书名去掉两端空白后保留 1 至 100 个字符;标签是最多 10 项的文本列表,每项去空白后保留 1 至 30 个字符;同名标签保留第一次。finished 接收 JSON 布尔值。字段拼错和附加的 owner_id 都交回错误。

# 项目文件:api_project/src/bookapi/schemas.py
from typing import Annotated, Literal

from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator

# Annotated 保留 str 的含义,再附上 Pydantic 应执行的规则。
Title = Annotated[str, Field(strict=True, min_length=1, max_length=100)]
Tag = Annotated[str, Field(strict=True, min_length=1, max_length=30)]
Tags = Annotated[list[Tag], Field(max_length=10)]
Flag = Annotated[bool, Field(strict=True)]


class InputModel(BaseModel):
    # 拒绝拼错字段和客户端偷偷附加的 owner_id。
    model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)


class BookCreate(InputModel):
    title: Title
    finished: Flag = False
    tags: Tags = Field(default_factory=list)

    @field_validator("tags")
    @classmethod
    def unique_tags(cls, value: list[str]) -> list[str]:
        # 保留标签首次出现的顺序;此时每项已经完成类型和长度校验。
        return list(dict.fromkeys(value))


class BookOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    title: str
    finished: bool
    tags: list[str]


class BookPage(BaseModel):
    total: int
    offset: int
    limit: int
    items: list[BookOut]

Field(strict=True) 让 Flag 不再把 1 或 "false" 转成布尔值。ConfigDict 设置模型行为;extra="forbid" 让额外字段成为错误,str_strip_whitespace=True 对文本执行去空白。InputModel 的子类继承这些设置。

default_factory=list 在每次创建模型时给出一份列表。@field_validator("tags") 默认在该字段的基本校验之后运行;函数收到的已经是合格的字符串列表。cls 对应当前模型类,装饰器按类方法方式调用它。dict.fromkeys 按首次出现的顺序取键,最后转回列表。

用 {"title":" Python Web ","tags":[" 网络 ","网络"]} 观察变化:书名先去空白成为 "Python Web";两个标签分别去空白成为同名文本;字段校验器再合并成 ["网络"]。输入 {"title":"A","finished":"false"} 则在布尔检查处失败,得不到 BookCreate。

用实际书名和重复标签逐步展示 JSON 解析、文本去空白、字段约束、标签去重和模型形成,同时显示错误布尔值的停止位置

BookCreate.model_validate(mapping) 接收字典并执行同样的检查;book.model_dump() 返回 Python 字典。模型内部的字段仍然是属性,可以写 book.title。需要交给 JSON 时,model_dump(mode="json") 会把日期等类型也转成 JSON 能表达的形式。这章的文本、布尔和列表两种模式结果相同。

模型方法与配置可以继续查阅 Pydantic 模型文档;校验器的位置与执行方式见 校验器文档。

笔记正文与全部题解 · Ctrl / ⌘ K