用 Python 类型注解驱动的数据验证 & 序列化框架
一句话:Pydantic 是 Python 中最流行的 数据验证与序列化 库,核心思想是用 Python 的类型注解来定义数据结构,并自动完成验证、转换、序列化。
传入任意原始数据(dict/JSON/字符串),Pydantic 自动按照你定义的类型进行验证,不合法就抛出详细的错误信息。
如果传入 "123" 但你声明了 int,Pydantic 会自动把字符串转成整数,而不是报错。
把 Pydantic 模型实例转成 dict、JSON 字符串等格式,可以自定义字段别名、排除字段、格式化输出。
自动生成 JSON Schema,这正是 FastAPI 能自动生成 Swagger 文档的原因,OpenAI Function Calling 也基于此。
from pydantic import BaseModel # 1. 定义模型(像写 dataclass 一样) class User(BaseModel): id: int name: str email: str # 2. 传入原始字典,自动验证 + 转换 user = User(id="42", name="Alice", email="alice@example.com") # id 被自动从 "42"(str) 转换成 42(int) ✅ print(user.id) # 42(int,不是字符串) print(user.name) # Alice # 3. 转换成 dict / JSON print(user.model_dump()) # {'id': 42, 'name': 'Alice', 'email': 'alice@example.com'}
传统 Python 没有强类型,接口边界的数据极容易出错。Pydantic 以极低成本解决了这个问题。
def create_user(data: dict) -> dict: # 手动验证每个字段,重复劳动,还容易漏 if 'id' not in data: raise ValueError("id is required") if not isinstance(data['id'], int): try: data['id'] = int(data['id']) except: raise ValueError("id must be integer") if 'name' not in data or not data['name']: raise ValueError("name is required") # ... 每个字段都要这样写 😩 return data # 错误信息不友好,没有字段路径,没有类型信息 # 嵌套对象更是噩梦...
from pydantic import BaseModel, EmailStr, field_validator class User(BaseModel): id: int # 自动验证 + 转换 name: str # 必填 email: EmailStr # 格式校验(需 pip install pydantic[email]) age: int | None = None # 可选字段 # 验证失败时,错误信息极其详细: try: User(id="abc", name="", email="not-email") except ValidationError as e: print(e) # 3 validation errors for User # id: Input should be a valid integer [type=int_parsing] # name: String should have at least 1 character # email: value is not a valid email address
Pydantic v2 核心逻辑用 Rust 重写(pydantic-core),比 v1 快 5-50 倍,是 Python 生态中性能最好的验证库之一。
FastAPI、SQLModel、LangChain、OpenAI SDK 等几乎所有主流 Python 框架都以 Pydantic 作为核心依赖。
基于类型注解,pyright / mypy / VSCode 都能完美补全、类型检查,彻底告别 dict 的 key 地狱。
语法和标准 Python dataclass 几乎相同,5 分钟上手,不需要学新的 DSL 或魔法方法。
Pydantic 的核心由以下几个层次构成,从模型定义到运行时验证,再到序列化输出。
Pydantic 支持多层次验证:内置类型、约束装饰器、自定义验证器、跨字段验证。
from pydantic import BaseModel from datetime import datetime, date from typing import List, Dict, Optional, Literal, Union from uuid import UUID from enum import Enum class Status(str, Enum): active = "active" inactive = "inactive" class Profile(BaseModel): user_id: UUID # UUID 格式 username: str age: Optional[int] = None # 可选 score: float is_vip: bool # "true"/"yes"/1 都能转换 tags: List[str] # 列表中每个元素也会被验证 metadata: Dict[str, int] # 字典键值类型验证 status: Status # Enum 枚举验证 created_at: datetime # ISO 字符串自动转 datetime role: Literal["admin", "user"] # 字面量限定
from pydantic import BaseModel, Field from typing import Annotated from pydantic import PositiveInt, NonNegativeFloat class Product(BaseModel): # 字符串约束 name: Annotated[str, Field(min_length=1, max_length=100)] sku: Annotated[str, Field(pattern=r'^[A-Z]{2}\d{4}$')] # 正则 # 数值约束 price: Annotated[float, Field(gt=0, le=99999.99)] # gt=大于, le=小于等于 quantity: PositiveInt # 内置正整数类型 discount: NonNegativeFloat = 0.0 # 非负浮点 # 列表约束 categories: Annotated[List[str], Field(min_length=1, max_length=5)] # 约束说明: # gt= 大于 # ge= 大于等于 # lt= 小于 # le= 小于等于 # multiple_of= 是...的倍数 # min_length= / max_length= 字符串/列表长度
from pydantic import BaseModel, field_validator, ValidationInfo class UserRegister(BaseModel): username: str password: str age: int @field_validator('username') @classmethod def username_must_be_valid(cls, v: str) -> str: if not v.isalnum(): raise ValueError('用户名只能包含字母和数字') return v.lower() # 同时做转换 @field_validator('password') @classmethod def password_strength(cls, v: str) -> str: if len(v) < 8: raise ValueError('密码长度至少 8 位') if not any(c.isdigit() for c in v): raise ValueError('密码必须包含数字') return v @field_validator('age') @classmethod def age_check(cls, v: int) -> int: if not (0 <= v <= 150): raise ValueError(f'年龄 {v} 不合理') return v
from pydantic import BaseModel, model_validator from datetime import date class DateRange(BaseModel): start_date: date end_date: date label: str = "" @model_validator(mode='after') # 所有字段验证完之后运行 def check_date_order(self) -> 'DateRange': if self.end_date < self.start_date: raise ValueError('end_date 必须大于等于 start_date') # 还可以自动生成字段 if not self.label: self.label = f"{self.start_date} ~ {self.end_date}" return self # 测试 r = DateRange(start_date="2024-01-01", end_date="2024-12-31") print(r.label) # "2024-01-01 ~ 2024-12-31"(自动生成)
Field() 是 Pydantic 的字段声明工具,用于设置默认值、文档描述、别名、约束等元数据。
from pydantic import BaseModel, Field from typing import Annotated class Article(BaseModel): # 基本默认值 title: str = Field(..., description="文章标题") # ... 表示必填 content: str = Field(..., min_length=10) views: int = Field(default=0, ge=0) # 动态默认值(用 default_factory) tags: List[str] = Field(default_factory=list) # 每次都是新列表 created: datetime = Field(default_factory=datetime.now) # 字段别名 — 输入时用 alias,输出时可控制 user_id: int = Field(..., alias="userId") # 接受 JSON 中的 userId page_num: int = Field(1, serialization_alias="pageNum") # 输出时用 pageNum # 排除字段(不序列化到 JSON) _secret: str = Field(default="", exclude=True) # 用 Annotated 更现代的写法 score: Annotated[float, Field(ge=0, le=100, description="评分 0-100")] # 使用别名时需要开启: class Config: populate_by_name = True # v2: 允许用字段名或 alias 都能赋值
Field(...) 中的 ...(省略号)表示必填,等同于没有默认值。有 default= 或 default_factory= 则为可选。
alias 同时影响输入输出;validation_alias 只影响输入解析;serialization_alias 只影响输出序列化。
可变默认值(list/dict)必须用 default_factory,否则所有实例共享同一个对象(Python 经典坑)。
Pydantic 支持任意深度的嵌套,子模型也会被自动验证;通过继承可以复用和扩展模型。
from pydantic import BaseModel from typing import List class Address(BaseModel): city: str province: str postcode: str class Order(BaseModel): order_id: int items: List[str] amount: float class User(BaseModel): name: str address: Address # 嵌套模型(对象) orders: List[Order] # 嵌套模型列表 # 传入字典,自动递归验证 user = User(**{ "name": "张三", "address": {"city": "深圳", "province": "广东", "postcode": "518000"}, "orders": [ {"order_id": 1, "items": ["iPhone"], "amount": 7999.0}, {"order_id": 2, "items": ["AirPods"], "amount": 1299.0}, ] }) print(user.address.city) # 深圳 print(user.orders[0].order_id) # 1(int,非字符串)
class UserBase(BaseModel): username: str email: str class UserCreate(UserBase): # 创建用户(需要密码) password: str class UserUpdate(UserBase): # 更新用户(所有字段可选) username: str | None = None email: str | None = None class UserResponse(UserBase): # 返回给前端(含 id,不含密码) id: int is_active: bool created_at: datetime class Config: from_attributes = True # v2: 允许从 ORM 对象转换
Pydantic 模型实例提供了丰富的序列化方法,可以精细控制输出内容和格式。
user = User(id=1, name="Alice", email="alice@example.com") # 转 dict user.model_dump() # → {'id': 1, 'name': 'Alice', 'email': 'alice@example.com'} # 只输出指定字段 user.model_dump(include={'id', 'name'}) # → {'id': 1, 'name': 'Alice'} # 排除字段 user.model_dump(exclude={'email'}) # → {'id': 1, 'name': 'Alice'} # 只输出非 None 字段 user.model_dump(exclude_none=True) # 使用序列化别名(serialization_alias) user.model_dump(by_alias=True) # 直接转 JSON 字符串 user.model_dump_json(indent=2) # → '{\n "id": 1,\n "name": "Alice",\n ...\n}' # 生成 JSON Schema(用于 API 文档/AI Function Calling) User.model_json_schema() # → {'title': 'User', 'type': 'object', 'properties': {...}, ...} # 从 JSON 字符串创建实例 user2 = User.model_validate_json('{"id": 2, "name": "Bob", "email": "b@b.com"}') # 从 ORM 对象创建(需开启 from_attributes=True) user3 = UserResponse.model_validate(orm_user_obj)
.dict() 和 .json(),v2 中改为 .model_dump() 和 .model_dump_json()。v2 保持了向后兼容,但 v1 API 在 v2 中被标记为废弃。2023 年发布的 v2 是破坏性更新,核心用 Rust 重写,性能提升巨大,但 API 有多处改动。
| 对比项 | v1 写法 | v2 写法 |
|---|---|---|
| 转 dict | user.dict() |
user.model_dump() |
| 转 JSON | user.json() |
user.model_dump_json() |
| 从 dict 创建 | User.parse_obj(d) |
User.model_validate(d) |
| 从 JSON 创建 | User.parse_raw(s) |
User.model_validate_json(s) |
| JSON Schema | User.schema() |
User.model_json_schema() |
| 字段定义方式 | class Config: ... |
model_config = ConfigDict(...) |
| 验证器 | @validator('field') |
@field_validator('field') |
| 跨字段验证 | @root_validator |
@model_validator(mode='after') |
| ORM 支持配置 | orm_mode = True |
from_attributes = True |
| 性能 | Pure Python | ⚡ Rust Core(快 5~50x) |
model_config = ConfigDict(...) 替代内部 class Config,语义更清晰,工具支持更好。from pydantic import BaseModel, ConfigDict class User(BaseModel): model_config = ConfigDict( from_attributes = True, # 支持 ORM 对象直接转换 populate_by_name = True, # 同时接受 alias 和字段名 str_strip_whitespace = True, # 字符串自动去首尾空格 validate_default = True, # 默认值也走验证 extra = 'forbid', # 禁止额外未声明字段(严格模式) ) id: int name: str
FastAPI 100% 基于 Pydantic,它将 Pydantic 模型直接用作请求体解析、参数验证和响应序列化,同时自动生成 OpenAPI 文档。
from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field, EmailStr from typing import Optional app = FastAPI() # ① 请求体模型 class UserCreate(BaseModel): username: str = Field(..., min_length=3, max_length=20) email: EmailStr = Field(..., description="用户邮箱") password: str = Field(..., min_length=8) # ② 响应模型(不暴露密码) class UserResponse(BaseModel): id: int username: str email: str # ③ 路由 —— FastAPI 自动: # a) 解析请求体并用 UserCreate 验证 # b) 用 UserResponse 序列化返回值 # c) 在 /docs 中自动生成 Swagger 文档 @app.post("/users", response_model=UserResponse) async def create_user(user: UserCreate): # user 已经是验证通过的 UserCreate 实例 db_user = save_to_db(user) return db_user # 自动过滤 password 字段
response_model 指定的 Pydantic 模型来过滤返回值,确保敏感字段(如 password、token)不会泄露,即使你的函数返回了完整的数据库对象。在实际项目中积累的 Pydantic 使用模式与避坑指南。
extra='forbid' 防止注入多余字段ConfigDict(extra='forbid') 拒绝任何未声明的字段,是防御性编程的好习惯。
description 注释model_validate 而非构造函数处理外部数据User(**dict) 和 User.model_validate(dict) 行为略有不同,处理外部输入推荐后者,语义更明确。
model_validator(mode='before')before 模式在字段级验证前运行,接受原始数据(可能是 dict 或 ORM 对象),需要手动处理类型,容易出错,优先使用 after 模式。
Annotated 定义自定义类型UserId = Annotated[int, Field(gt=0)],然后在多个模型中复用 UserId,比到处写 Field(gt=0) 更 DRY。
from_attributes=Truefrom_attributes=True 让 Pydantic 通过属性访问(而非字典下标)读取字段值。
from pydantic import BaseModel, Field from typing import Annotated # ✅ 定义可复用的约束类型 UserId = Annotated[int, Field(gt=0)] UserName = Annotated[str, Field(min_length=2, max_length=50)] Score = Annotated[float, Field(ge=0, le=100)] PageSize = Annotated[int, Field(default=20, ge=1, le=100)] # 在多个模型中复用 class UserProfile(BaseModel): id: UserId name: UserName score: Score class Leaderboard(BaseModel): user_id: UserId # 复用同一约束 score: Score # 复用同一约束 page: PageSize