Python 数据验证神器 Pydantic:数据解析与验证、设置管理、JSONSche、与ORM集成全搞定
使用Python类型注解进行数据验证和设置管理。
在 Python 生态里,数据验证和设置管理一直是绕不开的刚需。以前我们习惯手写 if not isinstance(x, int): raise TypeError,或者在 Django ORM 和 Flask 请求之间来回转换字典。直到 Pydantic 出现,它用 Python 原生类型注解把"解析、验证、序列化、文档生成"串成了一条流水线。无论你是写 FastAPI 接口、跑数据清洗脚本,还是管理微服务的配置,Pydantic 都能让代码少一半、错误早一步暴露。下面从安装到实战,把 Pydantic 的核心能力一次讲透。
1. 安装与快速上手:从一次类型验证开始
Pydantic 的安装非常简单,它依赖 Python 3.8 及以上版本,推荐使用 Pydantic v2(当前主流版本),性能和 API 都有大幅提升。
bash
pip install pydantic
# 如果需要邮箱、URL 等额外验证器
pip install "pydantic[email]"
安装完成后,先写一个最小可运行示例。Pydantic 的核心是 BaseModel,你只需要继承它并用类型注解声明字段。
python
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
age: int
is_active: bool = True
# 直接用字典构造,Pydantic 会自动解析和验证
user = User(id="1", name="Alice", age="30")
print(user)
print(type(user.id), type(user.age))
运行结果:
text
id=1 name='Alice' age=30 is_active=True
<class 'int'> <class 'int'>
注意这里传入了字符串 "1" 和 "30",Pydantic 在 v2 中默认启用了"宽松模式",会把能安全转换的字符串转成整数。如果传入无法转换的值,比如 age="abc",会立刻抛出 ValidationError:
python
from pydantic import ValidationError
try:
User(id=1, name="Bob", age="abc")
except ValidationError as e:
print(e)
输出会明确指出字段、输入值和错误原因:
text
1 validation error for User
age
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='abc', input_type=str]
这就是 Pydantic 的第一层价值:把"数据入口"变成一道自动化的类型闸门,而不是散落在业务代码里的手工判断。
2. 核心对象:BaseModel、Field 与模型配置
BaseModel 是 Pydantic 最常用的对象,但真正让模型可控的是 Field 和 model_config。它们决定了字段的默认值、约束、别名和序列化行为。
Field 的常用参数
Field 用于给字段附加元数据。以下是最常用的几个参数:
default:默认值,也可以用...表示必填。default_factory:默认值工厂,适合列表、字典等可变对象。alias:输入时的别名,常用于接收外部 API 的驼峰字段。ge、le、gt、lt:数值边界。min_length、max_length:字符串或列表长度。pattern:正则约束。description:字段说明,会进入 JSON Schema。
python
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(..., min_length=1, max_length=50, description="商品名称")
price: float = Field(..., gt=0, description="单价,必须大于 0")
tags: list[str] = Field(default_factory=list)
sku: str = Field(..., alias="skuCode")
model_config = {
"populate_by_name": True, # 允许用字段名或别名填充
"str_strip_whitespace": True, # 自动去除字符串首尾空格
}
p = Product(name=" Keyboard ", price="199.9", skuCode="KB-001")
print(p)
print(p.model_dump(by_alias=True))
运行结果:
text
name='Keyboard' price=199.9 tags=[] sku='KB-001'
{'name': 'Keyboard', 'price': 199.9, 'tags': [], 'skuCode': 'KB-001'}
这里 str_strip_whitespace=True 自动去掉了 name 两侧空格,populate_by_name=True 让我们既能用 sku 也能用 skuCode 构造对象。model_dump(by_alias=True) 则在输出时恢复成外部系统需要的别名格式。
模型配置 model_config
model_config 是一个字典,控制模型级别的行为。常见配置包括:
extra="forbid":禁止未声明字段,避免脏数据混入。frozen=True:模型不可变,适合配置对象。from_attributes=True:允许从 ORM 对象属性构造,这是与 ORM 集成的关键。validate_assignment=True:赋值时也触发验证。
python
class StrictUser(BaseModel):
model_config = {"extra": "forbid", "frozen": True}
id: int
name: str
try:
StrictUser(id=1, name="Tom", email="x@y.com")
except ValidationError as e:
print(e)
输出会提示 email 是多余字段。这种严格模式在配置文件解析中特别有用,能防止拼写错误被静默忽略。
3. 常用 API:验证、序列化与类型适配
Pydantic v2 把很多方法做了重命名,老项目迁移时要注意。下面按"输入---处理---输出"三个阶段梳理最常用的 API。
验证与解析
model_validate(obj):从字典或对象解析并验证。model_validate_json(json_str):直接从 JSON 字符串解析,比先json.loads再验证更高效。model_construct(**kwargs):跳过验证直接构造,仅用于可信数据,性能高但危险。
python
import json
from pydantic import BaseModel
class Item(BaseModel):
id: int
name: str
data = {"id": 1, "name": "Book"}
item1 = Item.model_validate(data)
item2 = Item.model_validate_json(json.dumps(data))
print(item1 == item2)
运行结果:
text
True
序列化与导出
model_dump():导出为字典。model_dump_json():导出为 JSON 字符串。model_dump(exclude={"password"}):排除敏感字段。model_dump(include={"id", "name"}):只导出指定字段。
python
class Account(BaseModel):
id: int
username: str
password: str
acc = Account(id=1, username="admin", password="secret")
print(acc.model_dump(exclude={"password"}))
print(acc.model_dump_json(include={"id", "username"}))
运行结果:
text
{'id': 1, 'username': 'admin'}
{"id":1,"username":"admin"}
类型适配与嵌套模型
Pydantic 支持嵌套模型、列表、字典、联合类型等复杂结构,并且会递归验证。
python
from typing import Optional, Union
from pydantic import BaseModel
class Address(BaseModel):
city: str
street: str
class Person(BaseModel):
name: str
age: Optional[int] = None
address: Address
contacts: list[str] = []
p = Person(
name="Lily",
address={"city": "Shanghai", "street": "Nanjing Rd"},
contacts=["13800000000", "lily@example.com"],
)
print(p.address.city)
print(p.model_dump())
运行结果:
text
Shanghai
{'name': 'Lily', 'age': None, 'address': {'city': 'Shanghai', 'street': 'Nanjing Rd'}, 'contacts': ['13800000000', 'lily@example.com']}
嵌套模型让复杂 JSON 的结构校验变得直观,任何一层出错都会带上完整路径,定位问题非常快。
4. 完整案例:用 Pydantic 解析一份订单 API 数据
光看零散 API 不够直观,下面用一个接近真实业务的订单解析案例,把验证、嵌套、别名、序列化和错误处理串起来。
假设外部系统推送的订单 JSON 如下:
json
{
"orderId": "A1001",
"customer": {"name": "张三", "phone": "13800000000"},
"items": [
{"sku": "KB-001", "qty": 2, "price": 199.9},
{"sku": "MS-002", "qty": 1, "price": 89.5}
],
"remark": null
}
我们用 Pydantic 建模并计算总价:
python
from typing import Optional
from pydantic import BaseModel, Field, field_validator
class Customer(BaseModel):
name: str = Field(..., min_length=1)
phone: str = Field(..., pattern=r"^1[3-9]\d{9}$")
class OrderItem(BaseModel):
sku: str
qty: int = Field(..., gt=0)
price: float = Field(..., gt=0)
@property
def subtotal(self) -> float:
return self.qty * self.price
class Order(BaseModel):
order_id: str = Field(..., alias="orderId")
customer: Customer
items: list[OrderItem] = Field(..., min_length=1)
remark: Optional[str] = None
model_config = {"populate_by_name": True}
@field_validator("items")
@classmethod
def check_total_qty(cls, v):
if sum(i.qty for i in v) > 100:
raise ValueError("单笔订单数量不能超过 100")
return v
@property
def total_amount(self) -> float:
return sum(i.subtotal for i in self.items)
raw = {
"orderId": "A1001",
"customer": {"name": "张三", "phone": "13800000000"},
"items": [
{"sku": "KB-001", "qty": 2, "price": 199.9},
{"sku": "MS-002", "qty": 1, "price": 89.5},
],
"remark": None,
}
order = Order.model_validate(raw)
print(order.order_id)
print(order.total_amount)
print(order.model_dump(by_alias=True))
运行结果:
text
A1001
489.3
{'orderId': 'A1001', 'customer': {'name': '张三', 'phone': '13800000000'}, 'items': [{'sku': 'KB-001', 'qty': 2, 'price': 199.9}, {'sku': 'MS-002', 'qty': 1, 'price': 89.5}], 'remark': None}
这个案例覆盖了真实开发中最常见的几个动作:别名映射、嵌套验证、正则校验、自定义验证器、计算属性和按别名导出。如果外部数据有问题,比如手机号格式错误,Pydantic 会直接告诉你 customer.phone 不匹配正则,而不需要你写一堆 if 去逐层判断。
5. 进阶技巧:自定义验证器、设置管理与 JSON Schema
Pydantic 的进阶能力主要体现在三块:自定义验证逻辑、BaseSettings 设置管理,以及自动生成 JSON Schema。
自定义验证器
v2 提供了 field_validator 和 model_validator。前者针对单个字段,后者针对整个模型。
python
from pydantic import BaseModel, field_validator, model_validator
class Range(BaseModel):
start: int
end: int
@field_validator("start", "end")
@classmethod
def non_negative(cls, v):
if v < 0:
raise ValueError("必须为非负数")
return v
@model_validator(mode="after")
def check_order(self):
if self.start >= self.end:
raise ValueError("start 必须小于 end")
return self
try:
Range(start=10, end=5)
except ValidationError as e:
print(e)
输出会指出模型级错误:Value error, start 必须小于 end。这种跨字段校验用 model_validator 最合适。
设置管理 BaseSettings
在 v2 中,BaseSettings 被移到了 pydantic-settings 包,需要单独安装:
bash
pip install pydantic-settings
它可以从环境变量、.env 文件读取配置,并自动做类型转换。
python
from pydantic_settings import BaseSettings, SettingsConfigDict
class AppSettings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_prefix="APP_")
debug: bool = False
database_url: str
max_connections: int = 10
# 假设环境变量 APP_DATABASE_URL=postgres://localhost/db
settings = AppSettings()
print(settings.database_url)
print(settings.debug)
如果环境变量缺失,启动时就会报错,而不是运行到一半才发现配置为空。这比 os.getenv 加手动转换可靠得多。
JSON Schema 生成
Pydantic 模型可以直接生成标准 JSON Schema,这对接口文档、前后端联调、低代码平台非常实用。
python
import json
from pydantic import BaseModel, Field
class UserCreate(BaseModel):
username: str = Field(..., min_length=3, description="用户名")
age: int = Field(..., ge=0, le=150)
schema = UserCreate.model_json_schema()
print(json.dumps(schema, ensure_ascii=False, indent=2))
输出片段:
json
{
"properties": {
"username": {"description": "用户名", "minLength": 3, "title": "Username", "type": "string"},
"age": {"maximum": 150, "minimum": 0, "title": "Age", "type": "integer"}
},
"required": ["username", "age"],
"title": "UserCreate",
"type": "object"
}
FastAPI 正是基于这个能力自动生成 OpenAPI 文档。你也可以把 schema 交给前端做表单校验,做到"一处定义,多处使用"。
6. 与 ORM 集成:从数据库对象到 Pydantic 模型
Pydantic 本身不是 ORM,但它和 SQLAlchemy、Django ORM、Tortoise ORM 配合非常顺。关键配置是 from_attributes=True,它允许 Pydantic 从对象的属性而不是字典取值。
以 SQLAlchemy 为例:
python
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, Session
from pydantic import BaseModel, ConfigDict
Base = declarative_base()
class UserORM(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
class UserSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
email: str
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
with Session(engine) as session:
session.add(UserORM(id=1, name="Alice", email="alice@example.com"))
session.commit()
orm_user = session.get(UserORM, 1)
user = UserSchema.model_validate(orm_user)
print(user.model_dump())
运行结果:
text
{'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}
如果没有 from_attributes=True,用 model_validate(orm_user) 会报错,因为 Pydantic 默认只认字典。加上这个配置后,ORM 对象、数据类、普通对象都能直接转换,省去了手写 to_dict() 的重复劳动。
在 Django 中也是类似思路:
python
class UserSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
username: str
user = User.objects.get(id=1)
schema = UserSchema.model_validate(user)
这种模式在 API 层特别常见:数据库查询返回 ORM 对象,Pydantic 负责校验和序列化,最后交给框架输出 JSON。职责清晰,也避免了 ORM 对象直接暴露带来的字段泄露风险。
7. 真实工作场景:接口入参、配置中心与数据清洗
Pydantic 在真实项目中的价值,往往体现在三个高频场景。
场景一:Web 接口入参校验
FastAPI 已经把 Pydantic 作为默认的数据模型。即使你不用 FastAPI,也可以在 Flask、Django 中手动调用。
python
from flask import Flask, request, jsonify
from pydantic import BaseModel, ValidationError
app = Flask(__name__)
class RegisterForm(BaseModel):
username: str
password: str
age: int
@app.post("/register")
def register():
try:
form = RegisterForm.model_validate(request.json)
except ValidationError as e:
return jsonify({"errors": e.errors()}), 400
return jsonify({"msg": "ok", "user": form.username})
e.errors() 返回结构化错误列表,前端可以直接按字段展示提示,不需要后端拼字符串。
场景二:配置中心与环境隔离
微服务通常有 dev、test、prod 多套配置。用 BaseSettings 结合环境变量,可以做到一套代码多环境运行。
python
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
app_name: str = "my-service"
redis_url: str
timeout: int = 30
settings = Settings()
在容器里通过 REDIS_URL 注入,本地用 .env 文件,代码完全不用改。类型错误会在启动阶段暴露,而不是等到第一次访问 Redis 才报错。
场景三:数据清洗与 ETL
处理 CSV、日志、第三方接口数据时,Pydantic 可以充当"数据质量守门员"。
python
from pydantic import BaseModel, field_validator
class LogRecord(BaseModel):
timestamp: str
level: str
message: str
@field_validator("level")
@classmethod
def normalize_level(cls, v):
v = v.upper()
if v not in {"INFO", "WARN", "ERROR"}:
raise ValueError("非法日志级别")
return v
raw_records = [
{"timestamp": "2024-01-01", "level": "info", "message": "start"},
{"timestamp": "2024-01-01", "level": "debug", "message": "x"},
]
valid = []
for r in raw_records:
try:
valid.append(LogRecord.model_validate(r))
except ValidationError as e:
print("跳过脏数据:", e.errors()[0]["msg"])
print("有效记录数:", len(valid))
运行结果:
text
跳过脏数据: Value error, 非法日志级别
有效记录数: 1
在 ETL 流程里,这种"验证失败就隔离"的策略能显著减少下游计算的异常。
8. 常见错误与总结
初学者用 Pydantic 时,最容易踩的坑集中在几个地方。
第一,混淆 v1 和 v2 的 API。v1 的 dict()、json()、parse_obj() 在 v2 中已改为 model_dump()、model_dump_json()、model_validate()。老教程里的写法在新版本会报 AttributeError。
第二,忘记 from_attributes=True。从 ORM 对象构造模型时,如果不加这个配置,会提示 Input should be a valid dictionary or instance of ...。
第三,可变默认值。不要写 tags: list = [],而应该用 Field(default_factory=list),否则所有实例可能共享同一个列表。
第四,过度使用 model_construct。它跳过验证,只适合内部可信数据,用错地方等于把验证闸门关掉了。
第五,把 BaseSettings 当成 BaseModel 用。v2 中它已经独立到 pydantic-settings,需要单独安装和导入。
总结一下,Pydantic 的核心竞争力在于:用 Python 类型注解这一种"单一事实来源",同时完成数据解析、验证、序列化、配置管理和 JSON Schema 生成。它不强迫你改变项目架构,却能在数据入口处提供一道可靠的自动化防线。对于任何需要处理外部数据、管理配置或对接 ORM 的 Python 项目,Pydantic 都值得成为默认依赖。先从一个 BaseModel 开始,把手工校验替换掉,你会很快感受到它带来的确定性和安全感。