Pydantic V1 和 V2 有什么区别?迁移时要注意什么?
简化版
Pydantic V2 最大的变化是把核心校验逻辑用 Rust 重写了(pydantic-core),校验速度普遍快 5~20 倍——这也是 FastAPI 0.100+ 全面转向 V2 的原因。但它带来了一批破坏性改名,迁移时最常撞到的是这几组:方法名——.dict() → .model_dump()、.json() → .model_dump_json()、.parse_obj() → .model_validate()、.parse_raw() → .model_validate_json()、.schema() → .model_json_schema();配置方式——内部类 class Config → model_config = ConfigDict(...),其中 orm_mode 改名成 from_attributes、allow_population_by_field_name 改成 populate_by_name;校验器——@validator → @field_validator(且必须配 @classmethod)、@root_validator → @model_validator(mode="before"/"after")。行为上也有几处静默变化,最容易踩的三个:① 默认值不再被校验(V1 里 Optional[int] = "abc" 会报错,V2 默认不校验默认值,要 validate_default=True);② Optional[X] 不再自动等于「可选字段」——V2 里 Optional[int] 只表示「可以是 None」,不给默认值它仍然是必填的,要可选必须写 Optional[int] = None;③ 严格的类型强转变松/变严(V2 有 strict 模式,且 str 不再接受 int 自动转换)。迁移工具用官方的 bump-pydantic 能自动改掉大部分改名。还要注意生态:pydantic-settings 从主包独立成了单独的包,需要 pip install pydantic-settings。核心记忆:V2 核心用 Rust 重写,快 5~20 倍;model_dump/model_validate/ConfigDict/field_validator 四组改名;Optional 不等于可选、默认值不再校验。
详细版
V1 → V2 对照速查:
| V1 | V2 | 说明 |
|---|---|---|
.dict() | .model_dump() | 转 dict |
.json() | .model_dump_json() | 转 JSON 字符串 |
.parse_obj(d) | .model_validate(d) | 从 dict 构造 |
.parse_raw(s) | .model_validate_json(s) | 从 JSON 构造 |
.schema() | .model_json_schema() | JSON Schema |
class Config | model_config = ConfigDict() | 配置 |
orm_mode | from_attributes | 从 ORM 对象构造 |
@validator | @field_validator | 要加 @classmethod |
@root_validator | @model_validator | 需指定 mode |
Field(regex=) | Field(pattern=) | 正则 |
Config.allow_mutation | model_config["frozen"] | 反义 |
# ① ★基本模型(V2 写法)★
from pydantic import BaseModel, Field, ConfigDict, field_validator, model_validator
from typing import Optional, Annotated
class UserIn(BaseModel):
model_config = ConfigDict( # ★★取代 class Config★★
from_attributes=True, # ★★V1 的 orm_mode★★
populate_by_name=True, # ★V1 的 allow_population_by_field_name★
str_strip_whitespace=True, # ★自动去空格★
extra="forbid", # ★★拒绝未知字段(防 Mass Assignment)★★
frozen=False,
)
name: str = Field(min_length=1, max_length=50)
email: str = Field(pattern=r"^[^@]+@[^@]+$") # ★★V1 是 regex=★★
age: int = Field(ge=0, le=150)
nickname: Optional[str] = None # ★★必须写 = None 才可选★★
tags: list[str] = Field(default_factory=list) # ★可变默认值用 factory★
@field_validator("name") # ★★V1 的 @validator★★
@classmethod # ★★V2 必须加★★
def name_not_admin(cls, v: str) -> str:
if v.lower() == "admin":
raise ValueError("不能叫 admin")
return v
@field_validator("email", mode="before") # ★before:在类型转换前★
@classmethod
def lower_email(cls, v):
return v.lower() if isinstance(v, str) else v
@model_validator(mode="after") # ★★V1 的 @root_validator★★
def check_consistency(self): # ★after 模式拿到的是实例★
if self.age < 18 and self.tags:
raise ValueError("未成年人不能设标签")
return self # ★★必须 return self★★
# ② ★常用操作★
u = UserIn.model_validate({"name": "x", "email": "A@B.com", "age": 20})
u = UserIn.model_validate_json('{"name":"x",...}')
u.model_dump() # → dict
u.model_dump(exclude_none=True, exclude={"age"}) # ★裁剪★
u.model_dump(mode="json") # ★★datetime → 字符串★★
u.model_dump_json()
u.model_copy(update={"age": 21}) # ★V1 的 .copy()★
UserIn.model_json_schema()
# ③ ★从 ORM 对象构造★
class UserOut(BaseModel):
model_config = ConfigDict(from_attributes=True) # ★★必须开★★
id: int
name: str
UserOut.model_validate(orm_user) # ★V1 的 from_orm★
# ④ ★Annotated 风格(★V2 推荐★)★
from typing import Annotated
Name = Annotated[str, Field(min_length=1, max_length=50)]
class Item(BaseModel):
name: Name # ★类型可复用★
# ⑤ ★computed_field(V2 新增)★
from pydantic import computed_field
class Rect(BaseModel):
w: float
h: float
@computed_field # ★★会出现在输出和 schema 里★★
@property
def area(self) -> float:
return self.w * self.h
# ⑥ ★pydantic-settings 独立了★
# pip install pydantic-settings
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
database_url: str
debug: bool = False
⚠️ 三个必须记住的点:①
Optional[X]在 V2 里不再自动意味着「这个字段可选」。V1 有一个隐式行为:Optional[int]会被当成「可选字段且默认为None」;V2 移除了这个隐式规则——Optional[int]只表示「值的类型可以是int或None」,如果不写= None,它仍然是必填字段(你必须显式传None进去)。这是迁移时最容易踩且最隐蔽的变化:代码不报错、schema 变了(字段从 optional 变成 required)、线上开始出现「缺少字段」的 422。②@field_validator必须配@classmethod,而且顺序是@field_validator在上、@classmethod在下。V1 的@validator内部自动做了 classmethod 包装,V2 要求显式声明;漏写会得到一个含义不明的报错。另外@model_validator(mode="after")拿到的是模型实例(不是 dict)且必须return self,而mode="before"拿到的是原始输入(通常是 dict)。③.model_dump()默认不做 JSON 兼容转换——datetime、Decimal、UUID会保持 Python 对象;要得到可直接json.dumps的结果得用.model_dump(mode="json")。V1 的.dict()行为与此类似但细节不同,迁移后如果把model_dump()的结果直接丢给json.dumps会抛TypeError。
完整版教学
一、V2 为什么快
★ 架构变化:
┌──────────────────────────────────────────────────────┐
│ ★V1★:纯 Python 实现校验逻辑 │
│ 每个字段的校验 = ★一串 Python 函数调用★ │
│ → 解释器开销大 │
├──────────────────────────────────────────────────────┤
│ ★V2★:pydantic(Python 层)+ ★pydantic-core(Rust)★ │
│ 模型定义时 ★编译出一个"校验器 schema"★ │
│ 运行时 ★整个校验在 Rust 里完成★,只在边界过一次 Python │
└──────────────────────────────────────────────────────┘
★ ★性能对比(量级)★:
┌────────────────────┬──────────┬──────────┐
│ 操作 │ V1 │ ★V2★ │
├────────────────────┼──────────┼──────────┤
│ 简单模型校验 │ 1x │ ★~5-10x★ │
│ 嵌套模型 │ 1x │ ★~10-20x★│
│ JSON 解析+校验 │ 1x │ ★~15x★ │
│ 序列化 model_dump │ 1x │ ★~3-5x★ │
└────────────────────┴──────────┴──────────┘
★ ★对 FastAPI 的实际影响★:
请求体校验和响应序列化是★每个请求都要做的★
→ ★高 QPS 接口能有明显的整体提升★
→ ★但如果瓶颈是数据库,提升感知不明显★
★ ★V2 的两阶段★:
① ★模型定义时(import 时)★:
构建 core schema → ★这一步比 V1 慢★
→ ★大量模型的项目启动会变慢一点★
② ★运行时★:校验在 Rust 里跑 → ★快很多★
★ 权衡:★启动慢一点,运行快很多★(对长期运行的服务是划算的)
★ ★JSON 解析的优化★:
V2 可以 ★直接从 JSON 字节流校验★,不用先 json.loads 再校验:
Model.model_validate_json(raw_bytes) # ★★一步到位★★
vs
Model.model_validate(json.loads(raw)) # 两步
★ FastAPI 内部对请求体用的就是前者
★ ★严格模式(V2 新能力)★:
class M(BaseModel):
model_config = ConfigDict(strict=True)
n: int
M(n="1") # ★★strict 下报错(V1/V2 默认都会转成 1)★★
# 也可以按字段:
n: int = Field(strict=True)
from pydantic import StrictInt
n: StrictInt
★ ★用途:API 里防止 "1" 和 1 混用导致的隐蔽 bug★
★ ★类型强转规则的变化★:
┌──────────────────────┬────────┬────────┐
│ 输入 → 目标类型 │ V1 │ ★V2★ │
├──────────────────────┼────────┼────────┤
│ 123 → str │ ★"123"★│ ★报错★ │
│ "123" → int │ 123 │ 123 │
│ "abc" → int │ 报错 │ 报错 │
│ 1.0 → int │ 1 │ 1 │
│ ★1.5 → int★ │ ★1★ │ ★报错★ │
│ "true" → bool │ True │ True │
└──────────────────────┴────────┴────────┘
★ ★V2 更严格:int → str 不再自动转、有精度损失的转换会报错★
→ ★迁移后可能出现新的 422★
V2 快的根本原因是把校验核心用 Rust 重写了(pydantic-core)——模型定义时编译出一个「校验器 schema」,运行时整个校验在 Rust 里完成。性能提升的量级是简单模型 510 倍、嵌套模型 1020 倍、JSON 解析加校验约 15 倍;对 FastAPI 的实际影响是「请求体校验和响应序列化每个请求都要做,高 QPS 接口有明显提升」——但如果瓶颈是数据库,感知就不明显。要知道 V2 有个权衡:模型定义时构建 core schema 比 V1 慢,所以大量模型的项目启动会变慢一点,换来运行时快很多。另外 V2 的类型强转更严格:123 → str 不再自动转、1.5 → int 会报错(V1 会静默截断成 1)——迁移后可能出现新的 422。
二、四组核心改名
★ ★① 实例方法★:
V1 V2
m.dict() → ★m.model_dump()★
m.json() → ★m.model_dump_json()★
m.copy() → ★m.model_copy()★
Model.parse_obj(d) → ★Model.model_validate(d)★
Model.parse_raw(s) → ★Model.model_validate_json(s)★
Model.from_orm(obj) → ★Model.model_validate(obj)★(配 from_attributes)
Model.schema() → Model.model_json_schema()
Model.construct() → ★Model.model_construct()★(跳过校验)
Model.update_forward_refs() → Model.model_rebuild()
m.__fields__ → ★m.model_fields★
m.__fields_set__ → m.model_fields_set
★ ★② 配置:class Config → model_config★:
# V1
class Config:
orm_mode = True
allow_population_by_field_name = True
anystr_strip_whitespace = True
validate_assignment = True
allow_mutation = False
schema_extra = {"example": {...}}
json_encoders = {datetime: lambda v: v.isoformat()}
# ★V2★
model_config = ConfigDict(
★from_attributes★=True, # orm_mode
★populate_by_name★=True, # allow_population_by_field_name
★str_strip_whitespace★=True, # anystr_strip_whitespace
validate_assignment=True,
★frozen★=True, # ★allow_mutation=False 的反义★
★json_schema_extra★={"example": {...}}, # schema_extra
# ★json_encoders 已弃用 → 用 field_serializer / Annotated★
)
★ ★③ Field 参数★:
V1 V2
regex= → ★pattern=★
min_items= → ★min_length=★(list 也用 min_length)
max_items= → max_length=
allow_mutation= → (移到 model_config 的 frozen)
const= → ★用 Literal["x"]★
unique_items= → ★移除★(要自己校验或用 Set)
★ 数值约束不变:gt / ge / lt / le / multiple_of
★ ★④ 校验器★:
# V1
@validator("name")
def check(cls, v, values, **kwargs): ...
@validator("name", pre=True)
@validator("name", always=True)
@root_validator
def check_all(cls, values): ...
# ★V2★
@field_validator("name")
★@classmethod★ # ★★必须★★
def check(cls, v: str) -> str: ...
@field_validator("name", ★mode="before"★) # pre=True
@field_validator("name", ★validate_default=True★) # always=True
@model_validator(★mode="before"★) # ★拿到原始输入(dict)★
@classmethod
def pre_check(cls, data): return data
@model_validator(★mode="after"★) # ★★拿到模型实例★★
def post_check(self): ...; ★return self★ # ★★必须返回 self★★
★ ★访问其他字段的方式变了★:
V1: def check(cls, v, values): values["other"]
V2: def check(cls, v, ★info: ValidationInfo★): ★info.data["other"]★
★ 注意:★info.data 只包含已校验过的字段★(顺序依赖)
四组改名要成组记忆。实例方法统一加了 model_ 前缀(model_dump、model_validate、model_copy、model_construct)。配置从内部类 class Config 改成 model_config = ConfigDict(...),其中三个高频改名是 orm_mode → from_attributes、allow_population_by_field_name → populate_by_name、allow_mutation=False → frozen=True(反义),而 json_encoders 已弃用(改用 field_serializer)。Field 参数里 regex → pattern、min_items → min_length、const 改用 Literal、unique_items 直接被移除了。校验器除了改名,还有两个行为变化:@field_validator 必须加 @classmethod、访问其他字段从 values 参数变成了 info.data(且只包含已校验过的字段,有顺序依赖)。
三、行为上的静默变化
★ ★★① Optional 不再自动可选(最隐蔽)★★:
# V1
class M(BaseModel):
x: Optional[int] # ★V1:可选,默认 None★
M() # ✓ V1 通过
# ★V2★
class M(BaseModel):
x: Optional[int] # ★★V2:必填!只是允许值为 None★★
M() # ★★V2 报错:Field required★★
M(x=None) # ✓
✓ 正确写法:x: Optional[int] = None
★ ★迁移后的表现:API 突然要求传一堆本来可以不传的字段 → 422★
★ 好消息:★这个变化让语义更清晰★("可以是 None" ≠ "可以不传")
★ ★② 默认值默认不校验★:
class M(BaseModel):
x: int = "abc" # ★V1 报错;V2 默认不校验,静默通过★
M().x # ★V2 返回 "abc"(类型是 str!)★
✓ model_config = ConfigDict(★validate_default=True★)
★ 设计理由:默认值是开发者写的,校验它是浪费(但会掩盖笔误)
★ ★③ 未知字段的处理★:
V1 默认 ★Extra.ignore★(忽略)
V2 默认 ★extra="ignore"★(同样忽略)
★ 但推荐都显式设成 ★extra="forbid"★:
→ ★前端传错字段名时立刻报错,而不是静默丢弃★
→ ★也是防 Mass Assignment 的关键★
★ ★④ 可变默认值★:
class M(BaseModel):
tags: list[str] = [] # ★pydantic 会深拷贝,不会像普通类那样共享★
★ 这点 V1/V2 都安全(★和普通 Python 类的可变默认参数陷阱不同★)
★ 但推荐用 default_factory=list(更明确)
★ ★⑤ 序列化的差异★:
m.model_dump() # ★datetime 保持 datetime 对象★
m.model_dump(★mode="json"★) # ★★datetime → ISO 字符串、UUID → str★★
m.model_dump_json() # 直接出 JSON 字符串
✗ json.dumps(m.model_dump()) # ★★TypeError: datetime not serializable★★
✓ json.dumps(m.model_dump(mode="json"))
✓ m.model_dump_json()
★ ★⑥ 校验错误的结构变了★:
# V1
[{"loc": ("body", "age"), "msg": "value is not a valid integer",
"type": "type_error.integer"}]
# ★V2★
[{"type": "int_parsing", "loc": ("body", "age"),
"msg": "Input should be a valid integer, unable to parse...",
"input": "abc", "url": "https://errors.pydantic.dev/..."}]
★ ★变化点★:
- ★type 的命名变了★(type_error.integer → int_parsing)
- ★新增 input 字段(★可能泄露用户输入★,对外时要过滤)
- ★新增 url 指向文档★
→ ★如果前端依赖 error type 做判断,迁移会break★
→ ★FastAPI 的 422 响应体结构也随之变化★
★ ★⑦ 嵌套模型的 smart union★:
V2 默认用 ★smart 模式★匹配 Union:
尝试找★最匹配★的成员,而不是 V1 的"从左到右第一个能通过的"
→ ★大部分情况更符合直觉,但个别场景结果不同★
✓ 需要旧行为:Field(union_mode="left_to_right")
七个静默变化里最隐蔽也最容易出事的是 Optional 语义变化——V2 移除了「Optional[X] 自动可选」的隐式规则,不写 = None 就是必填,迁移后的表现是「API 突然要求传一堆本来可以不传的字段」。默认值默认不再校验(x: int = "abc" 会静默通过,返回的是字符串),需要 validate_default=True 才恢复 V1 行为。序列化的差异要特别注意:model_dump() 不做 JSON 兼容转换,datetime 保持对象形态,直接 json.dumps 会 TypeError——要用 model_dump(mode="json") 或 model_dump_json()。校验错误的结构也变了:type 的命名规则不同(type_error.integer → int_parsing)、新增了 input 字段(可能泄露用户输入,对外要过滤)——如果前端依赖 error type 做判断,迁移会 break。
四、FastAPI 中的实际影响
★ ★FastAPI 版本对应★:
FastAPI < 0.100 → ★只支持 Pydantic V1★
FastAPI ≥ 0.100 → ★同时支持 V1 和 V2★
★ 过渡期可以:from pydantic.v1 import BaseModel # ★V2 包里的 V1 兼容层★
★ 但混用两套模型很痛苦 → ★尽早统一★
★ ★response_model 的行为★:
@app.get("/user", response_model=UserOut)
async def get_user():
return orm_user # ★需要 from_attributes=True★
★ V2 下 FastAPI 内部用 model_validate + model_dump(mode="json")
★ ★response_model 有序列化开销★:
→ ★大列表接口会明显★(每条都要过一遍校验+序列化)
→ 优化:★response_model_exclude_unset★、直接返回 dict、
或用 ★ORJSONResponse★
★ ★请求体校验★:
@app.post("/items")
async def create(item: ItemIn): # ★V2 的 model_validate_json★
...
★ ★422 响应结构变化★(见上)→ 前端可能要适配
★ ★Query/Path/Body 的 Annotated 写法(★现代推荐★)★:
# ★旧写法(仍可用但不推荐)★
async def f(q: str = Query(None, max_length=50)): ...
# ★★新写法(Annotated)★★
from typing import Annotated
async def f(q: Annotated[str | None, Query(max_length=50)] = None): ...
★ 好处:
① ★类型注解和默认值分离★(默认值就是普通的 Python 默认值)
② ★同一个 Annotated 类型可以复用★
③ ★静态检查工具能正确理解★
④ ★依赖也一样:Annotated[Session, Depends(get_db)]★
★ ★依赖注入的 Annotated 写法★:
# 旧
async def f(db: Session = Depends(get_db)): ...
# ★新(★可以定义可复用的类型别名★)★
DbDep = Annotated[AsyncSession, Depends(get_async_db)]
CurrentUser = Annotated[User, Depends(get_current_user)]
async def f(db: DbDep, user: CurrentUser): ...
★ ★大项目里这个写法能省掉大量重复★
★ ★自定义序列化(取代 json_encoders)★:
from pydantic import field_serializer, model_serializer
class M(BaseModel):
created: datetime
amount: Decimal
@field_serializer("created")
def ser_dt(self, v: datetime, _info):
return v.isoformat()
@field_serializer("amount")
def ser_amount(self, v: Decimal, _info):
return str(v) # ★★金额用字符串防精度丢失★★
★ ★自定义类型(V2 的新机制)★:
from pydantic import GetPydanticSchema
from pydantic_core import core_schema
class ObjectIdStr(str):
@classmethod
def __get_pydantic_core_schema__(cls, source, handler):
return core_schema.no_info_after_validator_function(
cls._validate, core_schema.str_schema())
@classmethod
def _validate(cls, v): ...
★ V1 是 __get_validators__,★V2 完全换了机制★
→ ★依赖自定义类型的库(如 ODMantic、旧版 beanie)需要升级★
FastAPI 的版本分界是 0.100——之前只支持 V1,之后同时支持两者(过渡期可以 from pydantic.v1 import BaseModel 用兼容层,但混用两套模型很痛苦,应该尽早统一)。要注意 response_model 有实实在在的序列化开销——每条数据都要过一遍校验加序列化,大列表接口会很明显,优化手段是 response_model_exclude_unset、直接返回 dict、或用 ORJSONResponse。Annotated 写法是现代推荐:Annotated[str | None, Query(max_length=50)] = None 把类型注解和默认值分离,而且依赖也能定义成可复用的类型别名(DbDep = Annotated[AsyncSession, Depends(get_async_db)]),大项目里能省掉大量重复。json_encoders 已弃用,改用 @field_serializer。最后,自定义类型的机制完全换了(__get_validators__ → __get_pydantic_core_schema__),依赖自定义类型的第三方库需要升级。
五、迁移实操
★ ★迁移步骤★:
① ★升级 FastAPI 到 ≥0.100★(先让两者能共存)
② ★跑 bump-pydantic 自动改名★:
pip install bump-pydantic
bump-pydantic myapp/
→ ★自动处理:方法改名、Config→ConfigDict、validator→field_validator★
③ ★人工检查 Optional 字段★(工具改不了语义)
④ ★检查 root_validator 的 mode★
⑤ ★跑测试,重点看 422 和序列化★
⑥ ★检查依赖的第三方库是否支持 V2★
★ ★bump-pydantic 改不了的(★必须人工★)★:
✗ ★Optional[X] 缺少 = None★(语义变化)
✗ ★依赖 values 参数顺序的复杂 validator★
✗ ★json_encoders 的自定义序列化★
✗ ★自定义类型的 __get_validators__★
✗ ★依赖 error type 字符串的前端/测试★
✗ ★动态创建模型(create_model)的细节★
★ ★第三方库的兼容性检查★:
┌──────────────────┬────────────────────────────────┐
│ ★SQLModel★ │ ★早期只支持 V1,0.0.14+ 支持 V2★│
│ ★pydantic-settings★│ ★★独立成包,要单独装★★ │
│ ★beanie / ODMantic★│ 需要对应版本 │
│ ★fastapi-users★ │ 检查版本 │
│ ★datamodel-codegen★│ 支持 --target-python-version │
└──────────────────┴────────────────────────────────┘
★ 检查方法:pip index versions <pkg> + 看 changelog
★ ★渐进式迁移(大项目)★:
# 阶段一:新代码用 V2,老代码用兼容层
from pydantic.v1 import BaseModel as BaseModelV1 # ★老模型★
from pydantic import BaseModel # ★新模型★
★ ✗ 注意:★V1 和 V2 的模型不能互相嵌套★
class New(BaseModel):
old: OldV1Model # ★★会报错★★
→ ★所以要按"模块"迁移,不能按"文件"随意混★
★ ★测试要重点覆盖的★:
□ ★必填/可选字段的边界★(Optional 语义变化)
□ ★类型强转★("1" → int、1.5 → int)
□ ★422 的响应结构★(如果前端依赖)
□ ★序列化输出★(datetime/Decimal 格式)
□ ★from_attributes 的 ORM 转换★
□ ★自定义校验器的错误信息★
★ ★值得顺手做的改进★:
✓ ★所有输入模型加 extra="forbid"★(防 Mass Assignment + 早发现前端传错)
✓ ★用 Annotated 重写 Query/Depends★
✓ ★输入输出模型分离★(UserIn / UserOut / UserInDB)
✓ ★金额用 Decimal + field_serializer 转字符串★
✓ ★datetime 统一 ISO 8601 + UTC★
迁移步骤是「升级 FastAPI → 跑 bump-pydantic 自动改名 → 人工处理语义变化 → 跑测试」。bump-pydantic 改不了的必须人工处理:Optional[X] 缺少 = None(语义变化,工具无法判断意图)、依赖 values 顺序的复杂 validator、json_encoders、自定义类型的 __get_validators__、以及依赖 error type 字符串的前端和测试。第三方库兼容性要逐个检查——尤其 pydantic-settings 独立成了单独的包、SQLModel 需要 0.0.14+。渐进式迁移有个硬限制:V1 和 V2 的模型不能互相嵌套,所以要按模块迁移而不是按文件随意混。迁移时值得顺手做几个改进:所有输入模型加 extra="forbid"、用 Annotated 重写依赖、输入输出模型分离、金额用 Decimal 加 field_serializer 转字符串。
六、实践清单
★ 改名速查(背下来):
┌────────────────────────────┬────────────────────────────┐
│ .dict() │ ★.model_dump()★ │
│ .json() │ ★.model_dump_json()★ │
│ .parse_obj() │ ★.model_validate()★ │
│ .parse_raw() │ ★.model_validate_json()★ │
│ .from_orm() │ .model_validate() + ★from_attributes★│
│ class Config │ ★model_config = ConfigDict()★│
│ orm_mode │ ★from_attributes★ │
│ @validator │ ★@field_validator + @classmethod★│
│ @root_validator │ ★@model_validator(mode=...)★│
│ Field(regex=) │ ★Field(pattern=)★ │
│ values 参数 │ ★info.data★ │
└────────────────────────────┴────────────────────────────┘
★ 检查清单:
□ ★所有 Optional 字段都写了 = None★
□ ★@field_validator 都加了 @classmethod★
□ ★@model_validator(mode="after") 都 return self★
□ ★输入模型设了 extra="forbid"★
□ ★from_attributes=True(需要从 ORM 构造时)★
□ ★model_dump 传 mode="json"(要给 json.dumps 时)★
□ ★pydantic-settings 单独安装了★
□ ★第三方库版本兼容 V2★
□ ★前端不依赖旧的 error type★
□ ★用 Annotated 写 Query/Depends★
★ 常见报错速查:
┌──────────────────────────────────────┬──────────────────┐
│ Field required(本来可选的字段) │ ★Optional 缺 = None★│
│ @classmethod 相关的怪异报错 │ ★field_validator 漏了★│
│ TypeError: Object of type datetime... │ ★model_dump 没传 mode★│
│ 'Config' is deprecated │ 改 ConfigDict │
│ ValidationError: Input should be a │ ★V2 类型更严格★ │
│ valid string(传了 int) │ │
│ MissingGreenlet / 无法从 ORM 构造 │ ★缺 from_attributes★│
└──────────────────────────────────────┴──────────────────┘
★ 一句话总结:
★"V2 把校验核心用 Rust 重写,快 5~20 倍;
迁移记住四组改名(model_dump / model_validate / ConfigDict /
field_validator+classmethod),
人工处理两个语义变化(Optional 不再自动可选、默认值不再校验),
并注意 model_dump 要传 mode='json' 才能直接给 json.dumps。"★
改名速查表值得背下来。检查清单里最关键的三条:所有 Optional 字段都写 = None、@field_validator 都加 @classmethod、输入模型设 extra="forbid"。报错速查表能快速定位:「本来可选的字段报 Field required」就是 Optional 缺 = None、「datetime 不能序列化」是 model_dump 没传 mode="json"、「无法从 ORM 构造」是缺 from_attributes。
记忆钩子:「★Pydantic V2 把校验核心用 Rust 重写了(pydantic-core),简单模型快 5
10 倍、嵌套模型 1020 倍、JSON 解析+校验约 15 倍★——这正是 FastAPI 0.100+ 转向 V2 的原因;代价是★模型定义时构建 core schema 比 V1 慢,所以启动稍慢、运行快很多★。★四组核心改名要成组记★:①★实例方法统一加 model_ 前缀★:.dict()→model_dump()、.json()→model_dump_json()、.parse_obj()→model_validate()、.parse_raw()→model_validate_json()、.from_orm()→model_validate()、.copy()→model_copy();②★配置:class Config → model_config = ConfigDict()★,其中 ★orm_mode→from_attributes★、★allow_population_by_field_name→populate_by_name★、★allow_mutation=False→frozen=True(反义)★、★json_encoders 弃用→改用 @field_serializer★;③★Field 参数:regex→pattern、min_items→min_length、const→用 Literal、unique_items 被移除★;④★校验器:@validator→@field_validator(★必须加 @classmethod★)、@root_validator→@model_validator(mode=‘before’/‘after’)★,★after 模式拿到的是模型实例且必须 return self★,★访问其他字段从 values 参数改成 info.data(且只含已校验过的字段)★。★两个最隐蔽的语义变化(工具改不了,必须人工)★:★① Optional[X] 不再自动可选★——V2 里它只表示『值可以是 None』,★不写 = None 仍然是必填★,迁移后表现是『API 突然要求传一堆本来可以不传的字段 → 422』;★② 默认值默认不再校验★(x: int = "abc"静默通过,要 validate_default=True)。★序列化要注意:model_dump() 不做 JSON 兼容转换,datetime 保持对象,直接 json.dumps 会 TypeError★ → ★用 model_dump(mode=‘json’) 或 model_dump_json()★。★类型强转更严格:123→str 不再自动转、1.5→int 会报错(V1 静默截断)★。★校验错误结构也变了★(type 命名从 type_error.integer 变成 int_parsing、★新增 input 字段可能泄露用户输入★),★前端若依赖 error type 会 break★。迁移工具用 ★bump-pydantic★ 能自动改名;★pydantic-settings 已独立成单独的包要单独装★;★V1 和 V2 的模型不能互相嵌套,所以要按模块迁移★。顺手改进:★输入模型加 extra=‘forbid’(防 Mass Assignment)★、★用 Annotated 写 Query/Depends 并定义可复用别名(DbDep = Annotated[AsyncSession, Depends(get_db)])★。」
七、常见误区与追问
- 误区:把
.dict()批量替换成.model_dump()就完成迁移了。 改名只是最表层的工作(而且bump-pydantic能自动完成),真正的坑是那些不报错的语义变化。最典型的是Optional[X]不再自动可选——V1 里x: Optional[int]表示「可选字段,默认None」,V2 里它只表示「值的类型可以是 int 或 None」,字段本身依然是必填的。迁移后代码能正常 import、测试如果没覆盖到就能通过,上线后开始大量返回 422「Field required」,而且报的还是那些用户从来不填的字段。第二个是默认值不再被校验(x: int = "abc"静默通过,运行时你拿到的是字符串),第三个是类型强转变严格(1.5 → int从静默截断变成报错)。所以迁移的正确姿势是:工具改名 → 人工逐个检查Optional字段 → 跑一遍覆盖了边界情况的测试。 - 误区:
@field_validator只是@validator换了个名字。 有三处实际差别。① 必须显式加@classmethod——V1 的@validator内部自动做了 classmethod 包装,V2 要求你自己写,而且顺序是@field_validator在上、@classmethod在下,写反了会报错。② 访问其他字段的方式变了——V1 是def check(cls, v, values)通过values字典拿,V2 改成def check(cls, v, info: ValidationInfo)通过info.data拿;注意info.data只包含「在当前字段之前已经校验成功的字段」,所以仍然有顺序依赖(字段的定义顺序决定校验顺序),并且如果前面的字段校验失败,它就不在data里。③pre=True改成mode="before"、always=True改成validate_default=True。另外@root_validator拆成了两种模式:mode="before"拿到的是原始输入(通常是 dict)、mode="after"拿到的是构造好的模型实例,而且必须return self(忘了返回会得到None)。 - 误区:
model_dump()出来的字典可以直接json.dumps()。 会抛TypeError: Object of type datetime is not JSON serializable。因为model_dump()默认是mode="python"——它返回的是「Python 对象形态」的字典,datetime还是datetime、Decimal还是Decimal、UUID还是UUID、枚举还是枚举成员。要得到可以直接序列化的结果,有两个选择:model_dump(mode="json")(返回字典,但所有值都转成了 JSON 兼容类型)或model_dump_json()(直接返回 JSON 字符串,性能也更好,因为序列化在 Rust 里完成)。这个区别在「把模型存进 Redis」「传给 Celery 任务」「写日志」这些场景下最容易踩到——因为这些地方通常不像 FastAPI 的response_model那样帮你处理好了。反过来,如果你要把数据存回数据库或传给另一个 Python 函数,mode="python"才是对的(保留类型信息)。 - 误区:V2 更快,所以升级后接口的 QPS 一定会明显提升。 取决于瓶颈在哪。Pydantic 的校验和序列化确实快了 5
20 倍,但它在一个典型请求里占的比例可能只有 515%——如果你的接口 80% 的时间花在等数据库、调下游服务,那么 Pydantic 快 10 倍带来的整体提升可能只有几个百分点。真正能感受到明显提升的场景是:① 请求体或响应体很大很复杂(深层嵌套的模型、上千条记录的列表);② 接口本身很轻(纯内存计算、缓存命中);③ QPS 很高(Pydantic 的 CPU 占用变成了可观的一部分)。另外还有个反方向的影响要知道:V2 在模型定义阶段(import 时)构建 core schema 比 V1 慢,所以模型特别多的大项目启动时间会变长——这对 serverless 冷启动是负面的,对长期运行的服务则完全划算。所以升级 V2 的主要理由其实是「生态在往那边走、V1 已进入维护模式」,性能是附带的好处。 - 误区:迁移时可以文件级混用 V1 和 V2 的模型,慢慢改。 V1 和 V2 的模型不能互相嵌套——如果一个 V2 的
BaseModel里有个字段类型是pydantic.v1.BaseModel的子类,会直接报错(反之亦然)。因为两者的校验核心完全不同,V2 无法把 V1 模型编译进自己的 core schema。所以「渐进式迁移」的粒度必须是模块级而不是文件级:把有依赖关系的一组模型(比如某个业务域的所有 schema)作为一个整体迁移,确保它们内部不跨版本引用。实践建议:先迁移叶子模块(不被别人嵌套的输出模型),再往上迁移;或者干脆一次性迁完——用bump-pydantic处理机械改名后,剩下的人工工作量通常没有想象中大。另外注意from pydantic.v1 import ...兼容层只是过渡手段,V1 已经进入维护模式,长期挂着两套会让新人困惑、也享受不到 V2 的性能。 - 追问:
model_config里哪些配置在实际项目中最该设? 五个。①extra="forbid"——用于所有输入模型:前端传了拼错的字段名时立刻报错(而不是静默丢弃导致「我明明传了为什么没生效」的排查地狱),同时这也是防 Mass Assignment 的关键(不让用户塞进is_admin之类的字段)。②from_attributes=True——用于所有从 ORM 对象构造的输出模型(V1 的orm_mode),不开的话model_validate(orm_obj)会失败。③str_strip_whitespace=True——自动去掉字符串两端空格,能省掉一大堆手写的.strip(),也避免「用户名末尾多个空格导致登录失败」这类问题。④validate_assignment=True——给模型属性赋值时也走校验(默认只在构造时校验),适合那些会被修改的领域模型。⑤frozen=True——让模型不可变(同时自动实现__hash__),适合配置对象和值对象。此外populate_by_name=True在用了alias(比如 API 用 camelCase、Python 用 snake_case)时几乎必开——它允许同时用字段名和别名来填充。 - 追问:Annotated 写法相比默认值写法好在哪? 四点。① 类型注解和「参数元信息」分离——旧写法
q: str = Query(None, max_length=50)把Query(...)塞进了默认值的位置,导致这个函数无法作为普通 Python 函数直接调用(直接调用时q会是一个Query对象而不是None);Annotated 写法q: Annotated[str | None, Query(max_length=50)] = None的默认值就是真正的None,函数可以脱离 FastAPI 正常调用和单元测试。② 可复用——可以定义类型别名:DbDep = Annotated[AsyncSession, Depends(get_async_db)]、CurrentUser = Annotated[User, Depends(get_current_user)],然后到处async def f(db: DbDep, user: CurrentUser),大项目里能省掉成百上千行重复,改依赖时也只需改一处。③ 静态检查友好——mypy/pyright 能正确理解参数类型(旧写法里默认值的类型和注解不匹配,类型检查器会报错或需要特殊处理)。④ 这是 FastAPI 官方现在推荐的写法,新文档全部用它。唯一的成本是稍微啰嗦一点,以及需要from typing import Annotated(Python 3.9+)。 - 追问:迁移后 422 的响应结构变了,前端怎么适配? 先看变化:V1 的错误项是
{"loc": [...], "msg": "value is not a valid integer", "type": "type_error.integer"},V2 变成了{"type": "int_parsing", "loc": [...], "msg": "Input should be a valid integer, unable to parse string as an integer", "input": "abc", "url": "https://errors.pydantic.dev/..."}。三个变化点:type的命名规则完全不同(type_error.integer→int_parsing)、新增了input字段(回显用户的原始输入)、新增了url(指向文档)。适配策略有三种。① 最推荐:不要让前端依赖type字符串——注册一个自定义的RequestValidationError处理器,把 Pydantic 的错误转成你自己稳定的错误格式(比如{"code": "validation_error", "fields": {"age": "必须是整数"}}),这样以后 Pydantic 再怎么变都不影响前端。② 如果必须保留原结构,至少要过滤掉input和url字段——input会把用户提交的原始内容回显出去,如果用户在密码字段传了错误类型,密码就会出现在错误响应里,这是实实在在的信息泄露风险。③ 灰度发布,让前端先兼容两种结构再切换。
八、加强记忆
Pydantic V2 把校验核心用 Rust 重写了(pydantic-core),简单模型快 510 倍、嵌套模型 1020 倍、JSON 解析加校验约 15 倍——这正是 FastAPI 0.100+ 转向 V2 的原因;代价是模型定义时构建 core schema 比 V1 慢,所以启动稍慢、运行快很多。四组核心改名要成组记忆:① 实例方法统一加 model_ 前缀(.dict() → model_dump()、.json() → model_dump_json()、.parse_obj() → model_validate()、.parse_raw() → model_validate_json()、.from_orm() → model_validate()、.copy() → model_copy());② 配置从 class Config 改成 model_config = ConfigDict(),其中 orm_mode → from_attributes、allow_population_by_field_name → populate_by_name、allow_mutation=False → frozen=True(反义)、json_encoders 弃用改用 @field_serializer;③ Field 参数:regex → pattern、min_items → min_length、const 改用 Literal、unique_items 被移除;④ 校验器:@validator → @field_validator(必须加 @classmethod)、@root_validator → @model_validator(mode="before"/"after"),after 模式拿到的是模型实例且必须 return self,访问其他字段从 values 参数改成了 info.data(且只包含已校验过的字段)。两个最隐蔽的语义变化(工具改不了,必须人工处理):① Optional[X] 不再自动可选——V2 里它只表示「值可以是 None」,不写 = None 仍然是必填,迁移后的表现是「API 突然要求传一堆本来可以不传的字段 → 422」;② 默认值默认不再校验(x: int = "abc" 会静默通过,需要 validate_default=True)。序列化要特别注意:model_dump() 不做 JSON 兼容转换,datetime 保持对象形态,直接 json.dumps 会 TypeError——要用 model_dump(mode="json") 或 model_dump_json()。类型强转也更严格:123 → str 不再自动转、1.5 → int 会报错(V1 是静默截断)。校验错误的结构同样变了(type 命名从 type_error.integer 变成 int_parsing、新增的 input 字段可能泄露用户输入),前端若依赖 error type 会 break。迁移工具用 bump-pydantic 能自动完成机械改名;pydantic-settings 已独立成单独的包,需要单独安装;V1 和 V2 的模型不能互相嵌套,所以要按模块迁移而不是按文件混用。顺手值得做的改进:输入模型加 extra="forbid"(防 Mass Assignment 并及早发现前端传错)、用 Annotated 写 Query 和 Depends 并定义可复用别名(DbDep = Annotated[AsyncSession, Depends(get_db)])。