跳转至

进阶 1 · pydantic v2

第 8 章你学了类型提示——它们运行时不强制。pydantic 把类型提示变成运行时强制的数据模型:声明字段类型的同时,自动获得校验、序列化、类型转换。它是 FastAPI 的地基,但也是个独立强大的库,值得单独一章。

Java 程序员一句话理解:pydantic = DTO + Bean Validation + Jackson,三合一,且全靠类型注解驱动。


1.1 安装与版本

uv add pydantic          # 或 pip install pydantic

本教程基于 pydantic v2(2023 年发布,底层用 Rust 重写,比 v1 快 5 到 50 倍)。v1 的很多 API(.dict().parse_obj()@validator)在 v2 已改名——遇到老代码注意迁移。


1.2 BaseModel:声明即校验

  • Java(DTO + 校验注解)

    public class Bookmark {
        @NotBlank @Size(max=200)
        private String title;
        @NotNull
        private String url;
        // + getter/setter + 构造器
    }
    
  • Python(pydantic)

    from pydantic import BaseModel
    
    class Bookmark(BaseModel):
        title: str
        url: str
    

声明字段后,构造时自动校验+转换:

bm = Bookmark(title="FastAPI", url="https://x.com")
print(bm.title)              # FastAPI
# 类型不匹配时,能转就转("42" → 42),不能就抛 ValidationError
bm2 = Bookmark(title="x", url=1)
print(bm2.url)               # "1"(int → str 自动转换)
# Bookmark(title="", )       # ❌ 缺 url → ValidationError

校验失败ValidationError,错误信息结构化、极详细:

from pydantic import ValidationError
try:
    Bookmark(title="x")      # 缺 url
except ValidationError as e:
    print(e.errors())
    # [{'type': 'missing', 'loc': ('url',), 'msg': 'Field required', ...}]

Java 程序员的视角

pydantic 把"校验失败"变成结构化异常(带字段定位、错误类型),比 Bean Validation 的 ConstraintViolationException 更易程序化处理。FastAPI 直接用它生成 422 响应(见第 2 章)。


1.3 默认值与必填

没默认值的字段必填,有默认值的可选:

class Bookmark(BaseModel):
    url: str                          # 必填
    title: str = "Untitled"           # 可选,默认
    tags: list[str] = []              # 默认空 list(pydantic 安全处理,不像普通函数默认值)

与第 2 章的区别

还记得"可变默认参数陷阱"吗?那是普通函数的问题。pydantic 的字段默认值是深拷贝的,每次实例化独立,不存在共享陷阱。可以放心写 tags: list[str] = []

Field 给默认值 + 描述:

from pydantic import Field

class Bookmark(BaseModel):
    title: str = Field(default="Untitled", description="书签标题")

1.4 字段约束:FieldAnnotated

  • Java(Bean Validation)

    @Size(min=1, max=200) String title;
    @Min(0) int stars;
    
  • Python(pydantic Field)

    class Bookmark(BaseModel):
        title: str = Field(min_length=1, max_length=200)
        stars: int = Field(ge=0)     # ge = >= 0
    

常用约束:min_length/max_length(字符串)、ge/gt/le/lt(数值大小)、pattern(正则)、max_digits 等。

特殊类型也自带校验,是 pydantic 的杀手锏:

from pydantic import BaseModel, HttpUrl, EmailStr

class Bookmark(BaseModel):
    url: HttpUrl          # 自动校验是合法 URL
    # owner: EmailStr     # 自动校验邮箱(需 pip install pydantic[email])

Bookmark(url="not-a-url", title="x")   # ❌ ValidationError(url 不合法)

贯穿项目 bookmarks-apischemas.py 正是用 HttpUrl 校验书签地址——非法 URL 直接 422。

现代写法 Annotated(推荐,类型与约束分离):

from typing import Annotated
from pydantic import Field, BaseModel

TitleStr = Annotated[str, Field(min_length=1, max_length=200)]

class Bookmark(BaseModel):
    title: TitleStr      # 可复用、可读性高

1.5 嵌套模型

模型可嵌套,自动递归校验——这是 pydantic 处理复杂 JSON 的核心能力:

class Address(BaseModel):
    city: str
    zip_code: str

class User(BaseModel):
    name: str
    address: Address          # 嵌套
    tags: list[str]           # 集合元素也校验

u = User(name="Alice", address={"city": "上海", "zip_code": "200000"}, tags=["a", "b"])
print(u.address.city)         # 上海(自动构造 Address)

从 JSON 字符串直接构造:

import json
u = User.model_validate(json.loads(raw))    # 或 User.model_validate_json(raw)

1.6 校验器:自定义校验逻辑

@field_validator 校验单个字段,@model_validator 校验字段间关系(对照 Java 的自定义校验注解):

from pydantic import BaseModel, field_validator, model_validator

class Signup(BaseModel):
    username: str
    password: str
    confirm: str

    @field_validator("username")
    @classmethod
    def username_alnum(cls, v: str) -> str:
        if not v.isalnum():
            raise ValueError("username must be alphanumeric")
        return v

    @model_validator(mode="after")
    def passwords_match(self) -> "Signup":
        if self.password != self.confirm:
            raise ValueError("passwords do not match")
        return self

要点

  • @field_validator 装饰器要加 @classmethod(v2 要求)。
  • mode="after" 表示在字段都已解析后运行(也可 "before" 做预处理)。
  • 校验器返回值会替换原值(可在此做规范化,如去空格、小写)。

1.7 序列化与反序列化

v2 的方法命名统一为 model_*

操作 方法 Java 对应
模型 → dict bm.model_dump() objectMapper.writeValueAsString
模型 → JSON bm.model_dump_json() writeValueAsString
dict → 模型 Bookmark.model_validate(d) readValue
JSON → 模型 Bookmark.model_validate_json(s) readValue(String)
bm = Bookmark(url="https://x.com", title="X")
bm.model_dump()          # → dict,如 {'url': <url 对象>, 'title': 'X'}
bm.model_dump_json()     # '{"url":"https://x.com","title":"X"}'

# 选择性输出(序列化控制)
bm.model_dump(include={"title"})      # 只保留 title
bm.model_dump(exclude={"url"})        # 排除 url

FastAPI 怎么用

FastAPI 的 response_model 自动调用 model_dump/JSON 序列化;请求体自动用 model_validate。你几乎不用手写序列化代码。


1.8 从 ORM 对象读取:from_attributes

数据库模型(SQLAlchemy)和 API 契约(pydantic)通常分离。from_attributes=True 让 pydantic 能从任意对象的属性构造(类似 MapStruct):

from pydantic import BaseModel, ConfigDict

class BookmarkOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)   # 开启
    id: int
    title: str

# 假设 db_bookmark 是 SQLAlchemy ORM 对象(有 .id .title 属性)
out = BookmarkOut.model_validate(db_bookmark)   # 直接从属性读

贯穿项目 bookmarks-api/schemas.py 的所有 *Out 模型都开了 from_attributes=True——路由返回 ORM 对象,FastAPI + pydantic 自动转成响应契约。


1.9 配置:ConfigDict

from pydantic import BaseModel, ConfigDict

class M(BaseModel):
    model_config = ConfigDict(
        str_strip_whitespace=True,   # 自动去首尾空格
        frozen=True,                 # 不可变(类似 dataclass frozen)
        extra="forbid",              # 禁止传入未声明字段(默认 ignore)
    )

常用:str_strip_whitespacefrozenextraignore/forbid/allow)、populate_by_name(允许字段别名)。


1.10 pydantic-settings:环境变量配置

这是 pydantic 在"配置管理"上的独立杀手级用途——对照 Java 的 @ConfigurationProperties + @Value

# pip install pydantic-settings
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", extra="ignore")

    database_url: str = "sqlite:///./app.db"
    debug: bool = False
    cors_origins: list[str] = ["http://localhost:5173"]

# 从环境变量 / .env 自动加载,类型转换自动完成
settings = Settings()

环境变量自动映射:DATABASE_URL=...settings.database_urlDEBUG=truesettings.debug == True(字符串自动转 bool)。

贯穿项目 bookmarks-api/config.py 正是用它管理数据库地址和 CORS——见 bookmarks-api/src/bookmarks_api/config.py

⚠️ Java 程序员的陷阱

不要手写 os.getenv("DATABASE_URL") 然后到处转换类型。用 pydantic-settings 一次性声明+校验+类型转换,缺失必填项时启动即报错(fail fast),而不是运行到一半才崩。


1.11 与 FastAPI 的衔接(预告)

第 2 章你会看到:FastAPI 把 pydantic 模型直接用在路由签名上——

@app.post("/bookmarks")
async def create(bm: BookmarkCreate):   # ← pydantic 模型做参数
    ...                                  # FastAPI 自动:解析 JSON、校验、422 报错

请求校验、响应序列化、OpenAPI 文档——全都从 pydantic 模型自动生成。这就是为什么 FastAPI 被称作"类型驱动"框架。


本章练习

练习 1.1

定义一个 User 模型:email(合法邮箱)、age(18–120)、role(默认 "user")。构造一个非法 age 的实例,捕获并打印 ValidationError 的字段定位。

参考答案

from pydantic import BaseModel, EmailStr, Field, ValidationError
class User(BaseModel):
    email: EmailStr
    age: int = Field(ge=18, le=120)
    role: str = "user"
try:
    User(email="a@b.com", age=5)
except ValidationError as e:
    print([err["loc"] for err in e.errors()])   # [('age',)]
EmailStrpip install pydantic[email]

练习 1.2

Signup 模型加一个 @field_validator,把 username 自动转小写并去首尾空格。

参考答案
from pydantic import BaseModel, field_validator
class Signup(BaseModel):
    username: str
    @field_validator("username")
    @classmethod
    def normalize(cls, v: str) -> str:
        return v.strip().lower()
练习 1.3

pydantic-settings 写一个 Settings,从环境变量读 PORT(int,默认 8000)和 DEBUG(bool,默认 False)。设 PORT=9000 DEBUG=true 后验证类型转换。

参考答案见 solutions/web/01_pydantic.py

← 回首页 | 下一章:进阶 2 · FastAPI 核心与异步