进阶 1 · pydantic v2¶
第 8 章你学了类型提示——它们运行时不强制。pydantic 把类型提示变成运行时强制的数据模型:声明字段类型的同时,自动获得校验、序列化、类型转换。它是 FastAPI 的地基,但也是个独立强大的库,值得单独一章。
Java 程序员一句话理解:pydantic = DTO + Bean Validation + Jackson,三合一,且全靠类型注解驱动。
1.1 安装与版本¶
本教程基于 pydantic v2(2023 年发布,底层用 Rust 重写,比 v1 快 5 到 50 倍)。v1 的很多 API(.dict()、.parse_obj()、@validator)在 v2 已改名——遇到老代码注意迁移。
1.2 BaseModel:声明即校验¶
-
Java(DTO + 校验注解)
-
Python(pydantic)
声明字段后,构造时自动校验+转换:
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 字段约束:Field 与 Annotated¶
-
Java(Bean Validation)
-
Python(pydantic Field)
常用约束: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-api 的 schemas.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 字符串直接构造:
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_whitespace、frozen、extra(ignore/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_url,DEBUG=true → settings.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',)]
EmailStr 需 pip install pydantic[email])
练习 1.2
给 Signup 模型加一个 @field_validator,把 username 自动转小写并去首尾空格。
练习 1.3
用 pydantic-settings 写一个 Settings,从环境变量读 PORT(int,默认 8000)和 DEBUG(bool,默认 False)。设 PORT=9000 DEBUG=true 后验证类型转换。
参考答案见 solutions/web/01_pydantic.py。
← 回首页 | 下一章:进阶 2 · FastAPI 核心与异步