← 返回题目列表

Pydantic V1 和 V2 有什么区别?迁移时要注意什么?

中等 第 19 / 27 题 更新于 2026/08/03
FastAPIPydanticV2迁移数据校验序列化

简化版

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 Configmodel_config = ConfigDict(...),其中 orm_mode 改名成 from_attributesallow_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 对照速查

V1V2说明
.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 Configmodel_config = ConfigDict()配置
orm_modefrom_attributes从 ORM 对象构造
@validator@field_validator要加 @classmethod
@root_validator@model_validator需指定 mode
Field(regex=)Field(pattern=)正则
Config.allow_mutationmodel_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] 只表示「值的类型可以是 intNone」,如果不写 = 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 兼容转换——datetimeDecimalUUID 会保持 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_dumpmodel_validatemodel_copymodel_construct)。配置从内部类 class Config 改成 model_config = ConfigDict(...),其中三个高频改名是 orm_modefrom_attributesallow_population_by_field_namepopulate_by_nameallow_mutation=Falsefrozen=True(反义),而 json_encoders 已弃用(改用 field_serializer)。Field 参数regexpatternmin_itemsmin_lengthconst 改用 Literalunique_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.dumpsTypeError——要用 model_dump(mode="json")model_dump_json()校验错误的结构也变了type 的命名规则不同(type_error.integerint_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、或用 ORJSONResponseAnnotated 写法是现代推荐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 重写依赖、输入输出模型分离、金额用 Decimalfield_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),简单模型快 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)])★。」

七、常见误区与追问

  • 误区:把 .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 只包含「在当前字段之前已经校验成功的字段」,所以仍然有顺序依赖(字段的定义顺序决定校验顺序),并且如果前面的字段校验失败,它就不在 datapre=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 还是 datetimeDecimal 还是 DecimalUUID 还是 UUID、枚举还是枚举成员。要得到可以直接序列化的结果,有两个选择:model_dump(mode="json")(返回字典,但所有值都转成了 JSON 兼容类型)或 model_dump_json()(直接返回 JSON 字符串,性能也更好,因为序列化在 Rust 里完成)。这个区别在「把模型存进 Redis」「传给 Celery 任务」「写日志」这些场景下最容易踩到——因为这些地方通常不像 FastAPI 的 response_model 那样帮你处理好了。反过来,如果你要把数据存回数据库或传给另一个 Python 函数,mode="python" 才是对的(保留类型信息)。
  • 误区:V2 更快,所以升级后接口的 QPS 一定会明显提升。 取决于瓶颈在哪。Pydantic 的校验和序列化确实快了 520 倍,但它在一个典型请求里占的比例可能只有 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.integerint_parsing)、新增了 input 字段(回显用户的原始输入)、新增了 url(指向文档)。适配策略有三种。① 最推荐:不要让前端依赖 type 字符串——注册一个自定义的 RequestValidationError 处理器,把 Pydantic 的错误转成你自己稳定的错误格式(比如 {"code": "validation_error", "fields": {"age": "必须是整数"}}),这样以后 Pydantic 再怎么变都不影响前端。② 如果必须保留原结构,至少要过滤掉 inputurl 字段——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_modefrom_attributesallow_population_by_field_namepopulate_by_nameallow_mutation=Falsefrozen=True(反义)json_encoders 弃用改用 @field_serializerField 参数regexpatternmin_itemsmin_lengthconst 改用 Literalunique_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.dumpsTypeError——要用 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)])。