KernelRanch
返回文章列表

pydantic v2 数据校验:9 条错误一次报完,和三个默认行为

2026-11-10 #Python

接口收上来的 JSON 和数据库里读出来的行,字段类型、取值范围、必填与否都要检查一遍。手写 if not isinstance(...) 很快会失控,pydantic 用类型注解描述结构,剩下的解析、转换、报错由它做:"123" 会转成 123,2026-11-10T09:30:00 会转成 datetime,不合法时一次性收集所有错误而不是遇到第一个就退出。反过来,如果只是解析一个两三层的小字典、或者数据完全来自可信的内部调用,引入一套模型类属于过度设计。

安装与实际版本

python -m venv D:\ai\cache\temp\py-pydantic
D:\ai\cache\temp\py-pydantic\Scripts\python.exe -m pip install pydantic
Name: pydantic
Version: 2.14.3
Location: D:\ai\cache\temp\py-pydantic\Lib\site-packages
import pydantic

print(pydantic.VERSION)
2.14.3

最小可运行示例

两个模型,一个嵌套另一个,加上字段约束、单字段校验器和计算字段:

from datetime import datetime
from typing import Literal

from pydantic import BaseModel, ConfigDict, Field, computed_field, field_validator


class Address(BaseModel):
    model_config = ConfigDict(extra="forbid")

    city: str = Field(min_length=1, max_length=32)
    zipcode: str = Field(pattern=r"^\d{6}$")


class User(BaseModel):
    model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)

    id: int = Field(gt=0)
    name: str = Field(min_length=2)
    email: str
    role: Literal["admin", "member"] = "member"
    signup_at: datetime
    address: Address
    tags: list[str] = []

    @field_validator("email")
    @classmethod
    def check_email(cls, v: str) -> str:
        if "@" not in v:
            raise ValueError("email 缺少 @")
        return v.lower()

    @computed_field
    @property
    def domain(self) -> str:
        return self.email.split("@", 1)[1]


u = User.model_validate(
    {
        "id": "7",
        "name": "  林可  ",
        "email": "LinKe@Example.COM",
        "signup_at": "2026-11-10T09:30:00",
        "address": {"city": "杭州", "zipcode": "310000"},
        "tags": ["python", "http"],
    }
)
print(u)
print("id type:", type(u.id).__name__, "| name:", repr(u.name))
print("domain:", u.domain)
id=7 name='林可' email='linke@example.com' role='member' signup_at=datetime.datetime(2026, 11, 10, 9, 30) address=Address(city='杭州', zipcode='310000') tags=['python', 'http'] domain='example.com'
id type: int | name: '林可'
domain: example.com

字符串 "7" 变成了整数,名字两端的空格被 str_strip_whitespace 去掉,邮箱被校验器转成小写。整个过程没有写一行类型判断。

校验失败时的输出是这套设计最值钱的地方。故意塞进 7 处错误,加上 extra="forbid" 产生的两处多余字段:

bad = {
    "id": 0,
    "name": "x",
    "email": "not-an-email",
    "role": "root",
    "signup_at": "昨天",
    "address": {"city": "", "zipcode": "31000", "country": "CN"},
    "unknown": 1,
}
try:
    User.model_validate(bad)
except ValidationError as e:
    print(e)
    print("error_count:", e.error_count())
9 validation errors for User
id
  Input should be greater than 0 [type=greater_than, input_value=0, input_type=int]
    For further information visit https://errors.pydantic.dev/2.14/v/greater_than
name
  String should have at least 2 characters [type=string_too_short, input_value='x', input_type=str]
    For further information visit https://errors.pydantic.dev/2.14/v/string_too_short
email
  Value error, email 缺少 @ [type=value_error, input_value='not-an-email', input_type=str]
    For further information visit https://errors.pydantic.dev/2.14/v/value_error
role
  Input should be 'admin' or 'member' [type=literal_error, input_value='root', input_type=str]
    For further information visit https://errors.pydantic.dev/2.14/v/literal_error
signup_at
  Input should be a valid datetime or date, input is too short [type=datetime_from_date_parsing, input_value='昨天', input_type=str]
    For further information visit https://errors.pydantic.dev/2.14/v/datetime_from_date_parsing
address.city
  String should have at least 1 character [type=string_too_short, input_value='', input_type=str]
    For further information visit https://errors.pydantic.dev/2.14/v/string_too_short
address.zipcode
  String should match pattern '^\d{6}$' [type=string_pattern_mismatch, input_value='31000', input_type=str]
    For further information visit https://errors.pydantic.dev/2.14/v/string_pattern_mismatch
address.country
  Extra inputs are not permitted [type=extra_forbidden, input_value='CN', input_type=str]
    For further information visit https://errors.pydantic.dev/2.14/v/extra_forbidden
unknown
  Extra inputs are not permitted [type=extra_forbidden, input_value=1, input_type=int]
    For further information visit https://errors.pydantic.dev/2.14/v/extra_forbidden
error_count: 9

address.zipcode 这样的路径直接指出嵌套位置,type= 后面的字符串(greater_than、string_pattern_mismatch)可以稳定地用于告警分类,比匹配人类可读的句子可靠。

常用 API 与参数

名称作用要点
BaseModel模型基类字段即类属性注解;实例化时校验
Field(...)字段约束gt/ge/lt/le、min_length/max_length、pattern、alias
field_validator单字段校验配 @classmethod;抛 ValueError 会变成一条错误
model_config = ConfigDict(...)模型级配置extra、str_strip_whitespace、validate_assignment、frozen
model_validate()校验 dict 或对象配 from_attributes=True 可吃 ORM 对象
model_validate_json()校验 JSON 字符串比先 json.loads 再 model_validate 快
model_dump()导出 dictinclude/exclude/exclude_computed_fields 控制输出
model_dump_json()导出 JSONdatetime 会变成 ISO 8601
computed_field派生字段出现在 repr、model_dump、JSON Schema 里
TypeAdapter校验裸类型不必为 list[int] 单独建模型

三个默认行为

extra 默认是 ignore。这意味着多传的字段不报错,直接消失:

class M(BaseModel):
    x: int

print(M(x=1, y="a").model_dump())
{'x': 1}

对外接口建议在模型上写 ConfigDict(extra="forbid"),否则上游改了字段名,服务这边静默丢数据,等到下游缺字段才发现。

赋值默认不校验。模型建好之后 u2.id = "not-a-number" 不会报错:

赋值后 id: 'not-a-number' -> 没有被拦下
validate_assignment: None

打开 validate_assignment=True 之后,同样的赋值会走一遍校验:

validate_assignment err: int_parsing Input should be a valid integer, unable to parse string as an integer

computed_field 会出现在 model_dump() 里。写序列化时如果只想要原始字段,得显式排除:

dump: {'x': 1, 'double': 2}
dump exclude_computed_fields: {'x': 1}

这三条都不是 bug,是 v2 的默认取向:尽可能宽容地接收数据,显式地收紧。问题是宽容的那一侧不会提醒你,所以配置项要按接口的对外程度来选。

一个迁移期报错

从 v1 迁过来的代码里常见 @validator,装上 2.14 之后仍然能跑,但会打警告:

Legacy(x='3').x = 6
PydanticDeprecatedSince20: Pydantic V1 style `@validator` validators are deprecated. You should migrate to Pydantic V2 style `@field_validator` validators, see the migration guide for more details. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.14/migration/

v2 里还留着 pydantic.v1 命名空间,让老代码能局部共存,但两套模型混用会在序列化和校验行为上分叉,迁移时最好按模块切干净。

小结

pydantic 的价值集中在两件事:把"数据长什么样"写成注解,把"哪里不对"写成带路径的错误列表。9 条错误一次报完,且每条都有稳定的 type 字符串,这是它能直接接进告警系统的原因。

几个坑的共同点是默认值偏宽容:多传字段被丢弃、赋值不校验、计算字段混进序列化结果。对外接口上把 extra="forbid" 和 validate_assignment 打开,比事后加断言省事;对内的小数据结构则保持默认,别为一次性解析写三个模型类。

数据来源与检索时间

检索时间 2026-11-10。版本、错误输出与各段运行结果为本机实测。

来源URL对应数据
PyPI《pydantic》项目页https://pypi.org/project/pydantic/当前版本 2.14.3(2026-11-04 发布);V2 为重写版本;pydantic.v1 命名空间用于渐进迁移
pydantic 官方文档《Models》https://docs.pydantic.dev/latest/concepts/models/model_validate/model_validate_json/model_dump/model_dump_json 的语义;extra 三个取值与默认 ignore;类型转换(coercion)策略;多错误一次性收集的行为
本机运行记录D:\ai\cache\temp\py-pydantic2.14.3;嵌套模型 9 条错误的完整文本;validate_assignment、computed_field、extra 三段对照;TypeAdapter 越界错误;v1 @validator 弃用警告原文

核验说明

  • 虚拟环境 D:\ai\cache\temp\py-pydantic,Python 3.11.9,pip install pydantic 装到 2.14.3。
  • 真跑的示例:嵌套模型的正例与 9 错反例、model_dump 的 python/json 两种模式与 include/exclude_computed_fields、model_validate_json、TypeAdapter 的 ge/le 校验、validate_assignment 开关对照、可变默认值不共享的验证、v1 @validator 的弃用警告。
  • 没测的部分:model_json_schema() 生成的 JSON Schema、泛型模型、RootModel、create_model 动态模型、from_attributes 吃 ORM 对象。这些在文档里有完整示例,但没有在本机验证,文章里没给结论。
  • 性能没测:没有对比 v1 与 v2 的校验耗时,也没有测 model_validate_json 相对 json.loads + model_validate 快多少。