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=Trueexclude_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.addressAddress 对象,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,不依赖隐式默认。


参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog