Pydantic 完全指南
Pydantic 是 Python 中最流行的数据验证库,基于 Python 类型注解(Type Hints)实现运行时数据验证、序列化和文档生成。 主要特点: 本文档基于 Pydantic v2。 Pydantic 会尽量将传入数据强制转换为声明的类型: Field() 用于对字段添加更细粒度的约束、默认值、描述等。 适合需要跨字段联合验证的场景。 当模型本身就是一个列表、字典等类型时使用。 FastAPI 深度集成 Pydantic,路由参数、请求体、响应体均使用 Pydantic 模型: .env 文件示例: 验证器是同步的,且会在每次构建模型时调
官方文档:https://docs.pydantic.dev/latest/
最后更新:2026-03-28
1. 基础概念
Pydantic 是什么
Pydantic 是 Python 中最流行的数据验证库,基于 Python 类型注解(Type Hints)实现运行时数据验证、序列化和文档生成。
主要特点:
- 利用标准 Python 类型注解,无额外学习成本
- 性能极高(v2 核心用 Rust 重写,比 v1 快 5-50 倍)
- 自动类型转换(如字符串
"123"自动转为int) - 丰富的内置验证器和自定义验证器支持
- 与 FastAPI、SQLAlchemy、Django 等深度集成
v1 与 v2 的区别
| 特性 | v1 | v2 |
|---|---|---|
| 核心实现 | 纯 Python | Rust(pydantic-core) |
| 性能 | 基准 | 快 5-50 倍 |
| 验证器装饰器 | @validator |
@field_validator |
| 模型配置 | class Config |
model_config = ConfigDict(...) |
| 序列化 | dict() / json() |
model_dump() / model_dump_json() |
| 私有字段 | PrivateAttr |
PrivateAttr(保留) |
本文档基于 Pydantic v2。
安装
pip install pydantic
# 可选依赖
pip install pydantic[email] # 邮箱验证支持
pip install pydantic[dotenv] # .env 文件支持(通过 pydantic-settings)
pip install pydantic-settings # 配置管理扩展
2. 基础使用
定义 BaseModel
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
age: int
email: str | None = None # 可选字段,默认 None
user = User(id=1, name="Alice", age=25)
print(user.id) # 1
print(user.name) # Alice
print(user.email) # None
自动类型转换
Pydantic 会尽量将传入数据强制转换为声明的类型:
user = User(id="1", name="Alice", age="25") # 字符串自动转为 int
print(user.id) # 1 (int)
print(user.age) # 25 (int)
验证失败
from pydantic import ValidationError
try:
User(id="abc", name="Alice", age=25)
except ValidationError as e:
print(e)
# 1 validation error for User
# id
# Input should be a valid integer, unable to parse string as an integer [...]
常用字段类型
from datetime import datetime
from decimal import Decimal
from pydantic import BaseModel
class Product(BaseModel):
id: int
name: str
price: Decimal
tags: list[str] = []
metadata: dict[str, str] = {}
created_at: datetime
is_active: bool = True
score: float | None = None
3. Field() — 字段约束
Field() 用于对字段添加更细粒度的约束、默认值、描述等。
常用参数
| 参数 | 类型 | 说明 |
|---|---|---|
default |
Any | 字段默认值 |
default_factory |
Callable | 动态默认值工厂函数 |
title |
str | 字段标题(用于文档) |
description |
str | 字段描述(用于文档/Schema) |
gt |
float | 大于(greater than) |
ge |
float | 大于等于(greater or equal) |
lt |
float | 小于(less than) |
le |
float | 小于等于(less or equal) |
min_length |
int | 字符串/列表最小长度 |
max_length |
int | 字符串/列表最大长度 |
pattern |
str | 字符串正则表达式约束 |
examples |
list | 示例值(用于 Schema) |
alias |
str | 字段别名(用于输入解析) |
serialization_alias |
str | 序列化时使用的别名 |
exclude |
bool | 序列化时排除此字段 |
frozen |
bool | 字段是否不可修改 |
示例
from pydantic import BaseModel, Field
from datetime import datetime
class Article(BaseModel):
id: int = Field(gt=0, description="文章 ID,必须大于 0")
title: str = Field(min_length=1, max_length=200, description="文章标题")
content: str = Field(min_length=10)
view_count: int = Field(default=0, ge=0)
tags: list[str] = Field(default_factory=list, max_length=10)
created_at: datetime = Field(default_factory=datetime.now)
author_id: int = Field(alias="authorId") # 接收 JSON 中的 authorId
# 使用别名传入
article = Article(
id=1,
title="Python 教程",
content="这是一篇详细的教程内容",
authorId=42
)
print(article.author_id) # 42
正则约束
from pydantic import BaseModel, Field
class User(BaseModel):
username: str = Field(pattern=r"^[a-zA-Z0-9_]{3,20}$")
phone: str = Field(pattern=r"^1[3-9]\d{9}$")
4. 常用内置类型
字符串类型
from pydantic import BaseModel
from pydantic.networks import EmailStr, AnyUrl, HttpUrl, AnyHttpUrl
class Contact(BaseModel):
email: EmailStr # 验证邮箱格式,需安装 email-validator
website: HttpUrl # 验证 HTTP/HTTPS URL
any_url: AnyUrl # 任意 URL
UUID 和 Path
from pathlib import Path
from uuid import UUID
from pydantic import BaseModel
class Resource(BaseModel):
id: UUID
file_path: Path
Annotated 类型约束(推荐写法)
from typing import Annotated
from pydantic import BaseModel, Field
# 用 Annotated 复用约束
PositiveInt = Annotated[int, Field(gt=0)]
ShortStr = Annotated[str, Field(max_length=50)]
class Product(BaseModel):
id: PositiveInt
name: ShortStr
stock: PositiveInt
Literal 枚举约束
from typing import Literal
from pydantic import BaseModel
class Order(BaseModel):
status: Literal["pending", "paid", "shipped", "completed", "cancelled"]
payment_method: Literal["alipay", "wechat", "card"]
5. 验证器
field_validator — 字段验证器
from pydantic import BaseModel, field_validator
class User(BaseModel):
name: str
age: int
email: str
@field_validator("name")
@classmethod
def name_must_not_be_empty(cls, v: str) -> str:
if not v.strip():
raise ValueError("姓名不能为空白字符串")
return v.strip()
@field_validator("age")
@classmethod
def age_must_be_adult(cls, v: int) -> int:
if v < 18:
raise ValueError("用户必须年满 18 岁")
return v
@field_validator("email")
@classmethod
def email_to_lowercase(cls, v: str) -> str:
return v.lower()
多字段共用同一个验证器
from pydantic import BaseModel, field_validator
class Address(BaseModel):
city: str
province: str
country: str
@field_validator("city", "province", "country")
@classmethod
def strip_whitespace(cls, v: str) -> str:
return v.strip()
model_validator — 模型级验证器
适合需要跨字段联合验证的场景。
from pydantic import BaseModel, model_validator
from typing import Self
class DateRange(BaseModel):
start_date: str
end_date: str
@model_validator(mode="after")
def check_date_order(self) -> Self:
if self.start_date > self.end_date:
raise ValueError("start_date 不能晚于 end_date")
return self
class PasswordForm(BaseModel):
password: str
confirm_password: str
@model_validator(mode="after")
def passwords_match(self) -> Self:
if self.password != self.confirm_password:
raise ValueError("两次密码不一致")
return self
mode="before" 与 mode="after"
from pydantic import BaseModel, field_validator
class Product(BaseModel):
price: float
# before: 在类型转换之前运行,接收原始输入
@field_validator("price", mode="before")
@classmethod
def parse_price_string(cls, v):
if isinstance(v, str):
v = v.replace("¥", "").replace(",", "").strip()
return v
# after: 在类型转换之后运行,接收已转换的值(默认)
@field_validator("price", mode="after")
@classmethod
def price_must_be_positive(cls, v: float) -> float:
if v <= 0:
raise ValueError("价格必须大于 0")
return v
field_serializer — 序列化钩子
from datetime import datetime
from pydantic import BaseModel, field_serializer
class Event(BaseModel):
name: str
created_at: datetime
@field_serializer("created_at")
def serialize_datetime(self, v: datetime) -> str:
return v.strftime("%Y-%m-%d %H:%M:%S")
event = Event(name="会议", created_at=datetime.now())
print(event.model_dump())
# {'name': '会议', 'created_at': '2026-03-28 10:00:00'}
6. 模型配置 ConfigDict
from pydantic import BaseModel, ConfigDict
class User(BaseModel):
model_config = ConfigDict(
str_strip_whitespace=True, # 自动去除字符串首尾空格
str_to_lower=False, # 字符串转小写
validate_default=True, # 对默认值也执行验证
validate_assignment=True, # 赋值时也触发验证
frozen=False, # True 则模型不可变(字段不可修改)
populate_by_name=True, # 允许同时使用字段名和别名
extra="forbid", # 禁止额外字段("allow"/"ignore"/"forbid")
use_enum_values=True, # 存储枚举的值而非枚举对象
arbitrary_types_allowed=True,# 允许非标准类型
)
id: int
name: str
extra 字段策略
from pydantic import BaseModel, ConfigDict
class StrictModel(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str
# 传入多余字段会报错
try:
StrictModel(name="Alice", unknown_field="value")
except Exception as e:
print(e) # Extra inputs are not permitted
class FlexModel(BaseModel):
model_config = ConfigDict(extra="allow")
name: str
m = FlexModel(name="Alice", extra_data="value")
print(m.model_extra) # {'extra_data': 'value'}
7. 序列化与反序列化
model_dump() — 转为字典
from pydantic import BaseModel, Field
class User(BaseModel):
id: int
name: str
password: str = Field(exclude=True) # 序列化时排除
email: str | None = None
user = User(id=1, name="Alice", password="secret", email="[email protected]")
user.model_dump()
# {'id': 1, 'name': 'Alice', 'email': '[email protected]'}
user.model_dump(exclude={"email"})
# {'id': 1, 'name': 'Alice'}
user.model_dump(include={"id", "name"})
# {'id': 1, 'name': 'Alice'}
user.model_dump(exclude_none=True)
# 排除所有值为 None 的字段
user.model_dump(exclude_unset=True)
# 只输出显式设置过的字段(默认值不算)
user.model_dump(by_alias=True)
# 使用 alias 作为 key
model_dump_json() — 转为 JSON 字符串
json_str = user.model_dump_json()
json_str = user.model_dump_json(indent=2)
json_str = user.model_dump_json(exclude_none=True)
model_validate() — 从字典创建
data = {"id": 1, "name": "Alice", "password": "xxx"}
user = User.model_validate(data)
model_validate_json() — 从 JSON 字符串创建
json_str = '{"id": 1, "name": "Alice", "password": "xxx"}'
user = User.model_validate_json(json_str)
model_json_schema() — 生成 JSON Schema
schema = User.model_json_schema()
import json
print(json.dumps(schema, indent=2, ensure_ascii=False))
8. 嵌套模型
基本嵌套
from pydantic import BaseModel
class Address(BaseModel):
street: str
city: str
country: str = "China"
class User(BaseModel):
id: int
name: str
address: Address
user = User(
id=1,
name="Alice",
address={"street": "中关村大街1号", "city": "北京"} # 自动转换为 Address
)
print(user.address.city) # 北京
列表嵌套
class Tag(BaseModel):
id: int
name: str
class Article(BaseModel):
title: str
tags: list[Tag] = []
article = Article(
title="Python 入门",
tags=[{"id": 1, "name": "Python"}, {"id": 2, "name": "编程"}]
)
递归模型
from __future__ import annotations
from pydantic import BaseModel
class Category(BaseModel):
id: int
name: str
children: list[Category] = []
Category.model_rebuild() # v2 中递归模型需调用此方法
9. 高级用法
泛型模型
from typing import Generic, TypeVar
from pydantic import BaseModel
T = TypeVar("T")
class Response(BaseModel, Generic[T]):
code: int = 200
message: str = "success"
data: T | None = None
class UserInfo(BaseModel):
id: int
name: str
# 使用
resp: Response[UserInfo] = Response(data=UserInfo(id=1, name="Alice"))
resp: Response[list[UserInfo]] = Response(data=[UserInfo(id=1, name="Alice")])
resp: Response[None] = Response(code=404, message="not found")
动态创建模型
from pydantic import create_model
# 动态定义字段
DynamicModel = create_model(
"DynamicModel",
name=(str, ...), # (类型, 默认值),... 表示必填
age=(int, 18),
email=(str | None, None),
)
obj = DynamicModel(name="Alice", age=25)
模型继承
from pydantic import BaseModel
class BaseUser(BaseModel):
id: int
name: str
class CreateUser(BaseUser):
id: int = None # 创建时可以不传 id
password: str
class UpdateUser(BaseModel):
name: str | None = None
password: str | None = None
class UserInDB(BaseUser):
hashed_password: str
is_active: bool = True
使用 Python 枚举
from enum import Enum
from pydantic import BaseModel, ConfigDict
class StatusEnum(str, Enum):
ACTIVE = "active"
INACTIVE = "inactive"
BANNED = "banned"
class User(BaseModel):
model_config = ConfigDict(use_enum_values=True)
name: str
status: StatusEnum = StatusEnum.ACTIVE
user = User(name="Alice", status="inactive")
print(user.status) # inactive(use_enum_values=True 存储值而非枚举对象)
print(type(user.status)) # <class 'str'>
私有属性
from pydantic import BaseModel, PrivateAttr
class Session(BaseModel):
user_id: int
_token: str = PrivateAttr(default="") # 私有属性,不参与验证和序列化
_cache: dict = PrivateAttr(default_factory=dict)
def set_token(self, token: str):
self._token = token
s = Session(user_id=1)
s.set_token("abc123")
print(s.model_dump()) # {'user_id': 1},不包含 _token
model_copy() — 复制模型并修改
user = User(id=1, name="Alice", age=25)
# 浅复制并修改字段
updated = user.model_copy(update={"name": "Bob", "age": 30})
print(updated.name) # Bob
print(user.name) # Alice(原对象不变)
Computed Fields — 计算字段
from pydantic import BaseModel, computed_field
class Rectangle(BaseModel):
width: float
height: float
@computed_field
@property
def area(self) -> float:
return self.width * self.height
@computed_field
@property
def perimeter(self) -> float:
return 2 * (self.width + self.height)
r = Rectangle(width=3.0, height=4.0)
print(r.area) # 12.0
print(r.model_dump()) # {'width': 3.0, 'height': 4.0, 'area': 12.0, 'perimeter': 14.0}
严格模式
from pydantic import BaseModel, ConfigDict
class StrictUser(BaseModel):
model_config = ConfigDict(strict=True)
id: int
name: str
# 严格模式不允许自动类型转换
try:
StrictUser(id="1", name="Alice") # 报错:id 必须是 int,不接受 str
except Exception as e:
print(e)
model_fields 和运行时内省
from pydantic import BaseModel, Field
class User(BaseModel):
id: int
name: str = Field(description="用户名")
# 获取所有字段信息
for field_name, field_info in User.model_fields.items():
print(f"{field_name}: {field_info.annotation}, required={field_info.is_required()}")
10. RootModel — 根类型模型
当模型本身就是一个列表、字典等类型时使用。
from pydantic import RootModel
class Tags(RootModel[list[str]]):
pass
tags = Tags.model_validate(["python", "pydantic", "fastapi"])
print(tags.root) # ['python', 'pydantic', 'fastapi']
print(tags.model_dump()) # ['python', 'pydantic', 'fastapi']
class UserMap(RootModel[dict[str, int]]):
pass
scores = UserMap.model_validate({"Alice": 90, "Bob": 85})
print(scores.root) # {'Alice': 90, 'Bob': 85}
11. 与常用库的配合
与 FastAPI 配合
FastAPI 深度集成 Pydantic,路由参数、请求体、响应体均使用 Pydantic 模型:
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class UserCreate(BaseModel):
name: str = Field(min_length=1, max_length=50)
email: str
age: int = Field(ge=0, le=150)
class UserResponse(BaseModel):
id: int
name: str
email: str
@app.post("/users", response_model=UserResponse)
async def create_user(user: UserCreate):
# FastAPI 自动验证请求体,验证失败返回 422
# response_model 自动过滤响应字段(如密码等敏感字段)
return UserResponse(id=1, name=user.name, email=user.email)
与 SQLAlchemy 配合(ORM 模式)
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import DeclarativeBase
from pydantic import BaseModel, ConfigDict
class Base(DeclarativeBase):
pass
class UserORM(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
# Pydantic 模型开启 ORM 模式,可直接从 ORM 对象创建
class UserSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
email: str
# 使用
orm_obj = UserORM(id=1, name="Alice", email="[email protected]")
user = UserSchema.model_validate(orm_obj) # 从 ORM 对象创建
print(user.model_dump())
与 pydantic-settings 配合(配置管理)
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env", # 从 .env 文件读取
env_file_encoding="utf-8",
case_sensitive=False, # 环境变量大小写不敏感
extra="ignore",
)
app_name: str = "MyApp"
debug: bool = False
database_url: str
secret_key: str
allowed_hosts: list[str] = ["localhost"]
max_connections: int = 10
# 单例模式(推荐)
from functools import lru_cache
@lru_cache
def get_settings() -> Settings:
return Settings()
settings = get_settings()
print(settings.database_url)
.env 文件示例:
DATABASE_URL=postgresql://user:pass@localhost/db
SECRET_KEY=your-secret-key
DEBUG=true
ALLOWED_HOSTS=["localhost", "example.com"]
与 Tortoise ORM 配合
from tortoise.models import Model
from tortoise import fields
from pydantic import BaseModel, ConfigDict
class UserModel(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=100)
email = fields.CharField(max_length=255)
class Meta:
table = "users"
# 手动定义 Pydantic Schema
class UserSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
email: str
# Tortoise 也有内置的 pydantic_model_creator
from tortoise.contrib.pydantic import pydantic_model_creator
UserPydantic = pydantic_model_creator(UserModel, name="User")
UserPydanticIn = pydantic_model_creator(UserModel, name="UserIn", exclude_readonly=True)
与 Redis / 缓存配合
import json
import redis
from pydantic import BaseModel
class UserCache(BaseModel):
id: int
name: str
permissions: list[str] = []
r = redis.Redis()
def cache_user(user: UserCache):
r.setex(f"user:{user.id}", 3600, user.model_dump_json())
def get_cached_user(user_id: int) -> UserCache | None:
data = r.get(f"user:{user_id}")
if data:
return UserCache.model_validate_json(data)
return None
12. 常用代码段
通用 API 响应体
from typing import Generic, TypeVar
from pydantic import BaseModel
T = TypeVar("T")
class ApiResponse(BaseModel, Generic[T]):
code: int = 200
message: str = "success"
data: T | None = None
@classmethod
def ok(cls, data: T = None, message: str = "success") -> "ApiResponse[T]":
return cls(code=200, message=message, data=data)
@classmethod
def error(cls, code: int = 400, message: str = "error") -> "ApiResponse[None]":
return cls(code=code, message=message, data=None)
# 使用
resp = ApiResponse.ok(data={"id": 1, "name": "Alice"})
resp = ApiResponse.error(code=404, message="用户不存在")
分页响应
from typing import Generic, TypeVar
from pydantic import BaseModel
T = TypeVar("T")
class PageResult(BaseModel, Generic[T]):
items: list[T]
total: int
page: int
page_size: int
@property
def total_pages(self) -> int:
return (self.total + self.page_size - 1) // self.page_size
@property
def has_next(self) -> bool:
return self.page < self.total_pages
时间戳字段自动处理
from datetime import datetime
from pydantic import BaseModel, Field, field_serializer
class TimestampMixin(BaseModel):
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
@field_serializer("created_at", "updated_at")
def serialize_dt(self, v: datetime) -> str:
return v.strftime("%Y-%m-%d %H:%M:%S")
class Article(TimestampMixin):
title: str
content: str
密码哈希(与 passlib 配合)
from pydantic import BaseModel, field_validator
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"])
class UserCreate(BaseModel):
username: str
password: str
@field_validator("password")
@classmethod
def hash_password(cls, v: str) -> str:
return pwd_context.hash(v)
从环境变量读取数据库配置
from pydantic_settings import BaseSettings
class DatabaseSettings(BaseSettings):
host: str = "localhost"
port: int = 5432
user: str = "postgres"
password: str
name: str
@property
def url(self) -> str:
return f"postgresql+asyncpg://{self.user}:{self.password}@{self.host}:{self.port}/{self.name}"
class Config:
env_prefix = "DB_" # 环境变量前缀:DB_HOST, DB_PORT...
将 ValidationError 转为可读信息
from pydantic import ValidationError
def format_validation_error(e: ValidationError) -> dict:
errors = {}
for error in e.errors():
field = " -> ".join(str(loc) for loc in error["loc"])
errors[field] = error["msg"]
return errors
try:
User(id="abc", name="")
except ValidationError as e:
print(format_validation_error(e))
# {'id': 'Input should be a valid integer...', 'name': '...'}
递归 dict 转换(深度序列化)
from pydantic import BaseModel
class Inner(BaseModel):
value: int
class Outer(BaseModel):
name: str
inner: Inner
obj = Outer(name="test", inner=Inner(value=42))
# 深度转换为原生 dict(嵌套也是 dict)
data = obj.model_dump()
print(type(data["inner"])) # dict
13. 最佳实践
分离 Create / Update / Response Schema
from pydantic import BaseModel, Field
# 创建时的 Schema(id 由服务器生成,不需要传入)
class UserCreate(BaseModel):
name: str = Field(min_length=1, max_length=100)
email: str
password: str = Field(min_length=8)
# 更新时全部字段可选
class UserUpdate(BaseModel):
name: str | None = Field(default=None, min_length=1, max_length=100)
email: str | None = None
# 返回给客户端的 Schema(不含密码)
class UserResponse(BaseModel):
id: int
name: str
email: str
is_active: bool
使用 Annotated 复用类型约束
from typing import Annotated
from pydantic import Field
# 定义复用类型
PositiveInt = Annotated[int, Field(gt=0)]
NonEmptyStr = Annotated[str, Field(min_length=1)]
PhoneStr = Annotated[str, Field(pattern=r"^1[3-9]\d{9}$")]
善用 exclude_unset 实现 PATCH 更新
from pydantic import BaseModel
class UserUpdate(BaseModel):
name: str | None = None
email: str | None = None
age: int | None = None
# PATCH 请求只传部分字段
payload = UserUpdate(name="Bob")
# exclude_unset=True 只返回用户实际传入的字段
update_data = payload.model_dump(exclude_unset=True)
print(update_data) # {'name': 'Bob'}(不包含 email 和 age)
# 然后用这些字段去更新数据库
# db_user.update(**update_data)
不要在 validator 中做 IO 操作
验证器是同步的,且会在每次构建模型时调用,不应在其中执行数据库查询、网络请求等 IO 操作。把这类逻辑放到 service 层:
# 不推荐
class UserCreate(BaseModel):
username: str
@field_validator("username")
@classmethod
def check_username_unique(cls, v):
# 不要在这里查数据库
if db.query(User).filter_by(username=v).first():
raise ValueError("用户名已存在")
return v
# 推荐:在 service 层做
async def create_user(data: UserCreate):
if await User.exists(username=data.username):
raise HTTPException(400, "用户名已存在")
return await User.create(**data.model_dump())
使用 model_config 统一配置
from pydantic import BaseModel, ConfigDict
class AppBaseModel(BaseModel):
"""项目内所有 Schema 的基类,统一配置。"""
model_config = ConfigDict(
str_strip_whitespace=True,
validate_default=True,
populate_by_name=True,
)
class User(AppBaseModel):
name: str
email: str
避免过度嵌套
层级过深的嵌套模型难以维护,建议超过 3 层时拆分为独立 Schema,并在文档中用 双链 标注关联关系。
14. 踩坑与注意事项
list / dict 默认值必须用 default_factory
from pydantic import BaseModel, Field
# 错误写法(共享同一个列表对象)
class Bad(BaseModel):
tags: list[str] = [] # Pydantic v2 实际上会处理这个,但仍推荐下面的写法
# 推荐写法
class Good(BaseModel):
tags: list[str] = Field(default_factory=list)
metadata: dict = Field(default_factory=dict)
alias 与字段名同时使用需开启 populate_by_name
from pydantic import BaseModel, ConfigDict, Field
class User(BaseModel):
model_config = ConfigDict(populate_by_name=True)
user_id: int = Field(alias="userId")
# 两种方式都可以
User(userId=1) # 使用 alias
User(user_id=1) # 使用字段名
Optional 与 required 的区别
from pydantic import BaseModel
class User(BaseModel):
# 必填字段(没有默认值)
name: str
# 可选字段(有默认值 None,可以不传)
nickname: str | None = None
# 可选字段(必须显式传入,但值可以是 None)
bio: str | None # 这个字段是 required 的,必须传,但可以传 None
v2 中 @validator 已废弃
# v1 写法(v2 中仍可用但会警告)
from pydantic import validator
class OldStyle(BaseModel):
name: str
@validator("name")
def check_name(cls, v):
return v
# v2 正确写法
from pydantic import field_validator
class NewStyle(BaseModel):
name: str
@field_validator("name")
@classmethod
def check_name(cls, v: str) -> str:
return v
model_dump() 嵌套模型默认展开为 dict
class Inner(BaseModel):
value: int
class Outer(BaseModel):
inner: Inner
obj = Outer(inner=Inner(value=1))
data = obj.model_dump()
print(data) # {'inner': {'value': 1}}
print(type(data["inner"])) # dict,不是 Inner 对象
frozen=True 时字段不可修改
from pydantic import BaseModel, ConfigDict
class ImmutableUser(BaseModel):
model_config = ConfigDict(frozen=True)
id: int
name: str
user = ImmutableUser(id=1, name="Alice")
user.name = "Bob" # 报错:Instance is frozen
最佳实践
用 model_validator(mode='before') 做跨字段预处理:在字段验证之前统一转换格式(如日期字符串标准化),避免每个字段的 field_validator 重复处理相同逻辑。
model_dump(exclude_none=True) 过滤空值:构建 API 请求体或 PATCH 操作时,用 exclude_none=True 或 exclude_unset=True 只输出用户明确设置的字段,避免覆盖服务端默认值。
用 TypeAdapter 验证非模型类型:需要验证 list[User]、dict[str, int] 等类型时,用 TypeAdapter 而非创建包装模型:
from pydantic import TypeAdapter
adapter = TypeAdapter(list[int])
adapter.validate_python([1, "2", 3.0]) # [1, 2, 3]
为 API 响应单独定义 Public Schema:数据库模型包含密码哈希等敏感字段,定义 UserPublic(BaseModel) 只暴露安全字段,作为 response_model。
model_config = ConfigDict(from_attributes=True) 支持 ORM 对象输入:与 SQLAlchemy/Tortoise-ORM 集成时必须设置,让 Pydantic 能从 ORM model 的属性中读取数据。
常见陷阱
陷阱:model_dump() 嵌套模型仍是 Pydantic 对象
现象: user.model_dump()['address'] 是 dict,但 user.dict()['address'] 也是 dict——这没问题。问题是直接访问 user.address 是 Address 对象,JSON 序列化时报错。
原因: 直接 json.dumps(user.model_dump()) 会因嵌套对象的日期、UUID 等类型报序列化错误。
解决: 用 user.model_dump(mode='json') 或 user.model_dump_json() 而非 json.dumps(user.model_dump())。
陷阱:validator(v1 写法)在 v2 中失效
现象: 迁移到 Pydantic v2 后,用 @validator('field') 装饰器的验证方法静默不执行。
原因: Pydantic v2 不向后兼容 v1 的 @validator,需改用 @field_validator,且签名有变化(类方法,第一个参数为值)。
解决: 用 from pydantic import field_validator 替换,或临时用 model_config = ConfigDict(arbitrary_types_allowed=True) 结合 v1_compat 模式。
陷阱:Optional[str] 不等于 str | None(Pydantic v2)
现象: v2 中 Optional[str] 字段在未传时默认值是 None 不需要显式赋值,但某些场景下字段值为 None 时验证报错。
原因: Pydantic v2 更严格区分"有值但为 None"和"未提供",Optional[str] 默认 default 仍需显式设置 = None。
解决: 始终显式声明默认值:name: str | None = None,不依赖隐式默认。