跳到主要内容

数据验证

数据验证是 FastAPI 的核心特性之一。通过 Pydantic,FastAPI 提供了强大的声明式数据验证功能,让你只需定义数据模型,就能自动获得验证、转换和文档生成。

Pydantic 基础

Pydantic 是一个数据验证库,使用 Python 类型注解来定义数据模型。FastAPI 内部使用 Pydantic 处理所有数据验证。

创建模型

from pydantic import BaseModel

class User(BaseModel):
"""用户模型"""
id: int
name: str
email: str
is_active: bool = True # 带默认值的字段

模型实例化

# 从字典创建
user = User(id=1, name="张三", email="[email protected]")

# 从 JSON 字符串创建
user = User.model_validate_json('{"id": 1, "name": "张三", "email": "[email protected]"}')

# 访问属性
print(user.name) # "张三"
print(user.is_active) # True(使用默认值)

模型转换为字典

# 转换为字典
user_dict = user.model_dump()
# {'id': 1, 'name': '张三', 'email': '[email protected]', 'is_active': True}

# 转换为 JSON 字符串
user_json = user.model_dump_json()
# '{"id": 1, "name": "张三", "email": "[email protected]", "is_active": true}'

字段类型

基本类型

from pydantic import BaseModel
from datetime import datetime, date, time, timedelta
from uuid import UUID
from pathlib import Path

class Demo(BaseModel):
# 数值类型
integer: int
floating: float
boolean: bool

# 字符串类型
text: str

# 时间类型
dt: datetime
d: date
t: time
td: timedelta

# 其他类型
uuid: UUID
path: Path

可选类型

from typing import Optional

class User(BaseModel):
# Python 3.10+ 语法
nickname: str | None = None

# 兼容语法
bio: Optional[str] = None

集合类型

from typing import List, Dict, Set

class Collections(BaseModel):
# 列表
tags: list[str]
# 或
items: List[int]

# 字典
metadata: dict[str, str]
# 或
scores: Dict[str, int]

# 集合
unique_ids: set[int]
# 或
ids: Set[int]

嵌套模型

class Address(BaseModel):
street: str
city: str
country: str = "中国"

class User(BaseModel):
name: str
address: Address # 嵌套单个模型
addresses: list[Address] # 嵌套模型列表

使用示例:

user = User(
name="张三",
address={"street": "南京路", "city": "上海"},
addresses=[
{"street": "北京路", "city": "北京"},
{"street": "广州路", "city": "广州"}
]
)

字段验证

Field 函数

使用 Field 函数添加验证规则和元数据:

from pydantic import BaseModel, Field

class Item(BaseModel):
name: str = Field(
..., # 必需字段
min_length=1, # 最小长度
max_length=100, # 最大长度
description="商品名称"
)

price: float = Field(
...,
gt=0, # 大于 0
le=10000, # 小于等于 10000
description="商品价格"
)

quantity: int = Field(
default=1, # 默认值
ge=0, # 大于等于 0
description="商品数量"
)

description: str | None = Field(
None,
max_length=500,
description="商品描述"
)

数值验证

参数含义示例
gt大于gt=0 值必须大于 0
ge大于等于ge=1 值必须大于等于 1
lt小于lt=100 值必须小于 100
le小于等于le=1000 值必须小于等于 1000
multiple_of是某数的倍数multiple_of=5 必须是 5 的倍数

字符串验证

from pydantic import BaseModel, Field

class User(BaseModel):
username: str = Field(
...,
min_length=3,
max_length=20,
pattern=r'^[a-zA-Z0-9_]+$' # 正则表达式
)

email: str = Field(
...,
pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$'
)

特殊字符串类型

Pydantic 提供了一些特殊的字符串类型:

from pydantic import BaseModel, EmailStr, HttpUrl, AnyUrl, IPvAnyAddress

class Contact(BaseModel):
email: EmailStr # 邮箱格式验证
website: HttpUrl # HTTP URL 验证
api_url: AnyUrl # 任意 URL 验证
server_ip: IPvAnyAddress # IP 地址验证

安装额外依赖:

pip install email-validator
# 或
pip install "pydantic[email]"

列表验证

from pydantic import BaseModel, Field

class Config(BaseModel):
tags: list[str] = Field(
default_factory=list, # 默认空列表(避免可变默认参数问题)
min_length=1, # 列表最小长度
max_length=10 # 列表最大长度
)

scores: list[int] = Field(
...,
min_length=3,
max_length=5
)

自定义验证器

field_validator

使用 @field_validator 装饰器添加自定义验证逻辑:

from pydantic import BaseModel, field_validator

class User(BaseModel):
name: str
username: str

@field_validator('username')
@classmethod
def username_alphanumeric(cls, v: str) -> str:
"""验证用户名只包含字母和数字"""
if not v.isalnum():
raise ValueError('用户名只能包含字母和数字')
return v.lower() # 转换为小写

@field_validator('name')
@classmethod
def name_must_not_empty(cls, v: str) -> str:
"""验证名称非空"""
if not v or not v.strip():
raise ValueError('名称不能为空')
return v.strip()

model_validator

验证整个模型或多个字段之间的关系:

from pydantic import BaseModel, model_validator

class Event(BaseModel):
start_date: date
end_date: date

@model_validator(mode='after')
def check_dates(self) -> 'Event':
"""验证结束日期必须晚于开始日期"""
if self.end_date <= self.start_date:
raise ValueError('结束日期必须晚于开始日期')
return self

验证前后处理

from pydantic import BaseModel, field_validator

class User(BaseModel):
username: str

@field_validator('username', mode='before')
@classmethod
def normalize_username(cls, v):
"""验证前预处理:去除空格并转小写"""
if isinstance(v, str):
return v.strip().lower()
return v

@field_validator('username', mode='after')
@classmethod
def check_username(cls, v):
"""验证后检查:确保不为空"""
if not v:
raise ValueError('用户名不能为空')
return v

模型配置

Config 类

使用内部 Config 类配置模型行为:

from pydantic import BaseModel

class User(BaseModel):
name: str
password: str

class Config:
# 额外字段处理
extra = 'forbid' # 禁止额外字段
# extra = 'ignore' # 忽略额外字段

# 字段别名
populate_by_name = True # 允许通过原字段名或别名访问

# 验证设置
validate_assignment = True # 赋值时也进行验证
validate_default = True # 验证默认值

# JSON Schema 示例
json_schema_extra = {
"example": {
"name": "张三",
"password": "secret"
}
}

model_config

Pydantic v2 推荐使用 model_config

from pydantic import BaseModel, ConfigDict

class User(BaseModel):
model_config = ConfigDict(
extra='forbid',
populate_by_name=True,
validate_assignment=True,
)

name: str
password: str

字段别名

定义别名

当 API 字段名与 Python 属性名不同时使用别名:

from pydantic import BaseModel, Field

class User(BaseModel):
name: str = Field(alias='userName')
full_name: str = Field(alias='fullName')

class Config:
populate_by_name = True # 允许同时使用别名和原名称

使用示例:

# 使用别名
user = User(userName='张三', fullName='张三丰')

# 如果设置了 populate_by_name=True,也可以使用原名称
user = User(name='张三', full_name='张三丰')

序列化别名

控制输出时的字段名:

class User(BaseModel):
name: str = Field(
alias='input_name', # 输入时的别名
serialization_alias='output_name' # 输出时的别名
)

默认值与默认工厂

默认值

class Item(BaseModel):
name: str
quantity: int = 0 # 简单默认值
is_active: bool = True

默认工厂

对于可变类型(如列表、字典),使用 default_factory

from pydantic import BaseModel, Field

class Item(BaseModel):
name: str
tags: list[str] = Field(default_factory=list) # 正确
metadata: dict = Field(default_factory=dict) # 正确

# 错误示范:可变默认值会被共享!
# tags: list[str] = [] # 不要这样做

工厂函数示例

from datetime import datetime
from uuid import uuid4

class Item(BaseModel):
id: str = Field(default_factory=lambda: str(uuid4()))
created_at: datetime = Field(default_factory=datetime.now)
tags: list[str] = Field(default_factory=list)

类型强制转换

Pydantic 会尝试将输入转换为声明的类型:

class Item(BaseModel):
quantity: int
price: float
is_active: bool

# 自动类型转换
item = Item(
quantity="42", # 字符串 "42" 转为整数 42
price="99.9", # 字符串转为浮点数
is_active="yes" # 字符串转为布尔值
)

print(item.quantity) # 42 (int)
print(item.price) # 99.9 (float)
print(item.is_active) # True (bool)

布尔值转换规则

以下值会被转换为 True

  • "True", "true", "1", "on", "yes"
  • 1, True

以下值会被转换为 False

  • "False", "false", "0", "off", "no"
  • 0, False

错误处理

当验证失败时,Pydantic 抛出 ValidationError

from pydantic import BaseModel, ValidationError

class User(BaseModel):
name: str
age: int

try:
User(name="张三", age="abc")
except ValidationError as e:
print(e.error_count()) # 错误数量
print(e.errors()) # 错误详情列表
print(e.json()) # JSON 格式的错误信息

错误信息示例:

[
{
"type": "int_parsing",
"loc": ["age"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "abc"
}
]

完整示例:商品模型

from datetime import datetime
from decimal import Decimal
from enum import Enum
from typing import Annotated
from uuid import UUID, uuid4

from pydantic import BaseModel, Field, EmailStr, field_validator, model_validator


class Category(str, Enum):
"""商品分类"""
ELECTRONICS = "electronics"
CLOTHING = "clothing"
FOOD = "food"
BOOKS = "books"


class Review(BaseModel):
"""商品评论"""
user_id: UUID
rating: Annotated[int, Field(ge=1, le=5, description="评分 1-5")]
comment: str | None = Field(None, max_length=500)
created_at: datetime = Field(default_factory=datetime.now)


class Product(BaseModel):
"""商品模型"""
model_config = ConfigDict(
extra='forbid',
json_schema_extra={
"example": {
"name": "智能手机",
"price": "2999.00",
"category": "electronics",
"stock": 100
}
}
)

id: UUID = Field(default_factory=uuid4, description="商品唯一ID")
name: Annotated[str, Field(
min_length=1,
max_length=100,
description="商品名称"
)]
description: str | None = Field(None, max_length=2000)
price: Decimal = Field(
...,
gt=0,
decimal_places=2,
description="商品价格"
)
category: Category
stock: int = Field(default=0, ge=0, description="库存数量")
is_available: bool = True
tags: list[str] = Field(default_factory=list)
reviews: list[Review] = Field(default_factory=list)
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)

@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('tags')
@classmethod
def tags_unique(cls, v: list[str]) -> list[str]:
"""标签去重"""
return list(set(v))

@model_validator(mode='after')
def check_availability(self) -> 'Product':
"""库存为0时自动设置为不可用"""
if self.stock == 0:
self.is_available = False
return self

小结

本章我们学习了:

  1. Pydantic 基础:创建模型、实例化、序列化
  2. 字段类型:基本类型、可选类型、集合类型、嵌套模型
  3. 字段验证:数值验证、字符串验证、列表验证
  4. 自定义验证器@field_validator@model_validator
  5. 模型配置:Config 类、model_config
  6. 字段别名:输入别名、序列化别名
  7. 默认值:默认值、默认工厂

Pydantic 的优势:

  • 类型安全的自动转换
  • 声明式的验证规则
  • 清晰的错误信息
  • 与 FastAPI 无缝集成
  • 自动生成 OpenAPI 文档

练习

  1. 创建一个用户注册模型,包含用户名、邮箱、密码(需要确认)
  2. 实现一个日期范围模型,验证结束日期晚于开始日期
  3. 创建一个商品模型,价格必须是正数且保留两位小数
  4. 实现自定义验证器,验证密码强度(至少8位,包含大小写字母和数字)