3.1 用Pydantic表达输入规则

2026-09-05 15:06 更新

用Pydantic表达输入规则

请求体是接口和调用方之间的契约。只在路由函数里写 if,会让长度、状态和值域规则散落在业务代码中;把这些规则写进 Pydantic 模型,FastAPI 就能在进入业务函数前完成解析和校验,并生成同一份 OpenAPI 描述。本节使用 Pydantic v2,案例仍然是文章管理 API。

先把客户端能决定的字段写出来

文章创建请求需要标题、正文、一个必须出现但可以为 null 的发布时间,以及可选标签。文章状态只能是 draftpublished,预计阅读时间有明确的数值范围。作者 ID、创建时间和审核结果由服务端决定,不属于这个输入模型;把服务端字段混入请求模型,会让客户端看起来可以修改不应该修改的数据。

Field 可以表达字符串长度、数值上下界和列表长度,枚举可以把有限值域写成类型。下面的完整程序还验证了默认值、缺少字段、显式 null 和非法值。

<!-- file: ch03_contracts/input_rules.py -->

from __future__ import annotations

from datetime import datetime
from enum import Enum

from pydantic import BaseModel, Field, StrictInt, ValidationError


class ArticleStatus(str, Enum):
    DRAFT = "draft"
    PUBLISHED = "published"


class PostCreate(BaseModel):
    title: str = Field(min_length=1, max_length=120)
    content: str = Field(min_length=1, max_length=10_000)
    status: ArticleStatus = ArticleStatus.DRAFT
    reading_time_minutes: int = Field(default=1, ge=1, le=120)
    # 没有默认值:键必须出现,但值可以是 null。
    published_at: datetime | None
    tags: list[str] = Field(default_factory=list, max_length=5)


class StrictPostCreate(BaseModel):
    title: str
    # 这一字段拒绝把字符串 "3" 转换为整数。
    reading_time_minutes: StrictInt = Field(ge=1, le=120)


def self_check() -> None:
    valid = PostCreate(
        title="先定义文章契约",
        content="把可检查的规则放在请求模型中。",
        published_at=None,
        tags=["fastapi", "pydantic"],
    )
    assert valid.status is ArticleStatus.DRAFT
    assert valid.reading_time_minutes == 1

    # 默认的 int 字段允许 Pydantic v2 做常见的类型转换。
    converted = PostCreate(
        title="类型转换",
        content="阅读时间来自 JSON 字符串。",
        reading_time_minutes="3",
        published_at=None,
    )
    assert converted.reading_time_minutes == 3

    # published_at 没有默认值,因此省略键会失败;写成 null 则合法。
    try:
        PostCreate(title="缺少字段", content="没有 published_at")
    except ValidationError as exc:
        assert any(error["loc"] == ("published_at",) for error in exc.errors())
    else:
        raise AssertionError("缺少 published_at 应该失败")

    try:
        PostCreate(
            title="",
            content="正文",
            reading_time_minutes=0,
            published_at=None,
            status="archived",
        )
    except ValidationError as exc:
        locations = {error["loc"] for error in exc.errors()}
        assert {("title",), ("reading_time_minutes",), ("status",)} <= locations
    else:
        raise AssertionError("非法字段应该失败")

    try:
        StrictPostCreate(title="严格输入", reading_time_minutes="3")
    except ValidationError as exc:
        assert exc.errors()[0]["loc"] == ("reading_time_minutes",)
    else:
        raise AssertionError("StrictInt 不应接受字符串")

    print("input_rules self-check passed")


if __name__ == "__main__":
    self_check()

在保存文件的目录中运行:

python ch03_contracts/input_rules.py

预期输出为 input_rules self-check passed。这次检查覆盖了模型创建和校验分支,没有启动 HTTP 服务;FastAPI 接口收到请求体时会使用同一个模型。

默认值、可空字段和缺省字段不是一回事

下面三种声明的含义不同:

required_text: str
nullable_required_text: str | None
optional_text: str | None = None

required_text 必须有字符串值;nullable_required_text 仍然必须在输入中出现,但值可以是 nulloptional_text 可以不出现,缺少时模型会填入 None。Pydantic v2 不会因为类型包含 None 就自动提供默认值,所以创建模型时应按业务语义选择声明方式。

同样要区分“转换”与“严格拒绝”。普通的 int 字段可以把能够明确解释的 JSON 字符串转换成整数,这适合兼容表单、旧客户端或配置输入;StrictIntStrictStr 等严格类型会拒绝这种转换,适合协议必须保持精确类型的字段。严格校验不是越多越好:先确认客户端契约和兼容要求,再在边界字段上启用。

枚举和边界规则要与业务含义一致

ArticleStatus 限制了可接受的状态值,但它没有判断“已经发布的文章必须有发布时间”这类跨字段业务规则。后者可能依赖当前时间、作者权限或数据库状态,应放在服务层或明确的业务校验中,而不是在字段校验器里查询数据库。字段模型负责输入形状,业务服务负责需要外部状态的判断。

长度与数量边界也需要有理由。max_length=10_000 是本案例的教学限制,不代表所有文章都必须使用这个值;生产项目应结合数据库列类型、网关限制、存储费用和编辑器策略设定,并在接口文档中说明。空列表默认使用 default_factory=list,避免不同模型实例共享同一个可变对象。

需要邮箱校验时再安装额外依赖

Pydantic 的 EmailStr 依赖 email-validator,基础的 Pydantic 安装不一定包含它。确实需要邮箱语义校验时,安装可选依赖并在模型中使用:

python -m pip install "pydantic[email]"

from pydantic import BaseModel, EmailStr


class AuthorCreate(BaseModel):
    email: EmailStr

不要为了检查一个普通字符串就引入这个依赖;如果项目已经决定接收邮箱地址,使用 EmailStr 比手写不完整的正则更容易维护。邮箱格式校验也不等于邮箱所有权验证,后者需要业务流程和外部确认。

FastAPI 中的失败边界

当路由参数声明为 payload: PostCreate 时,FastAPI 会在调用路由函数前构造模型。缺少必填字段、长度超限、枚举值不在集合中等输入错误会在请求边界返回 422,业务函数不会被调用。模型成功构造后,文章标题是否重复、作者是否有发布权限等问题仍应由服务层决定;不要把数据库查询塞进字段 validator 来“提前校验”。

输入模型还应只暴露客户端真正可以提交的字段。即使客户端额外发送 author_idcreated_at,服务端也应在模型配置或业务层明确决定是拒绝还是忽略,不能因为模型没有声明这些字段就默认把它们写进数据库。

资料来源

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号