pydantic v2 数据校验:9 条错误一次报完,和三个默认行为
接口收上来的 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() | 导出 dict | include/exclude/exclude_computed_fields 控制输出 |
model_dump_json() | 导出 JSON | datetime 会变成 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-pydantic | 2.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快多少。