Skip to main content
今日目标:API 的安全性始于”入口安检”。今天我们将深入学习 Pydantic,它是 FastAPI 的心脏。学会定义数据模型,让 API 能够自动拒绝那些缺字段、格式错误或类型不对的”脏数据”。今天不只是写代码,而是要彻底理解 “为什么需要数据验证”“Pydantic 如何自动验证数据” 以及 “如何设计合理的 Schema”

学习内容 (30 mins)

在开始写代码前,先搞懂这些核心概念,否则后面的代码你会看得云里雾里。
什么是 Pydantic?Pydantic 是一个数据验证库,使用 Python 类型提示来验证数据。它是 FastAPI 的核心依赖,负责验证请求和响应数据。为什么需要数据验证?
  • 安全性:防止恶意数据注入
  • 数据完整性:确保数据格式正确
  • 开发效率:自动验证,减少手动检查代码
  • 文档生成:自动生成 API 文档
传统方式 vs Pydantic传统方式(Flask/Django)
# 需要手动检查每个字段
if 'username' not in request.json:
    return {"error": "username is required"}, 400
if len(request.json['username']) < 3:
    return {"error": "username too short"}, 400
if not isinstance(request.json.get('age'), int):
    return {"error": "age must be integer"}, 400
# ... 无数个 if 判断
Pydantic 方式(FastAPI)
# 定义一个模型,自动验证
class UserCreate(BaseModel):
    username: str = Field(..., min_length=3)
    age: int = Field(..., gt=0, lt=120)

@app.post("/users/")
def create_user(user: UserCreate):
    # 如果代码能执行到这里,数据一定是合法的!
    return {"username": user.username}
Pydantic 的优势
  • 声明式:定义模型即可,无需写验证逻辑
  • 自动转换:自动将字符串转换为整数、日期等
  • 详细错误:提供详细的验证错误信息
  • 类型安全:基于 Python 类型提示,IDE 支持好
Schema(模式)Schema 是数据的”说明书”,定义了数据的结构和验证规则。
  • 定义方式:继承 pydantic.BaseModel
  • 字段类型:使用 Python 类型提示(str、int、float、bool 等)
  • 验证规则:使用 Field() 定义更细的规则
Field(字段)Field() 用于给字段添加验证规则和元数据。
  • ...:表示必填字段(Ellipsis)
  • min_length:字符串最小长度
  • max_length:字符串最大长度
  • gt:大于(greater than)
  • lt:小于(less than)
  • ge:大于等于(greater or equal)
  • le:小于等于(less or equal)
  • description:字段描述(会显示在 Swagger 文档中)
请求模型 vs 响应模型
  • 请求模型(Request Model):定义前端发送的数据结构
    • 通常包含所有字段,包括密码等敏感信息
    • 示例:UserCreate(创建用户时的数据)
  • 响应模型(Response Model):定义返回给前端的数据结构
    • 通常不包含敏感信息(如密码)
    • 使用 response_model=UserResponse 自动过滤字段
    • 示例:UserResponse(返回用户信息,不含密码)
EmailStr 和特殊类型Pydantic 提供了很多特殊类型,用于常见的数据验证:
  • EmailStr:自动验证邮箱格式(需要安装 email-validator
  • IPvAnyAddress:验证 IP 地址格式
  • HttpUrl:验证 URL 格式
  • datetime:自动解析日期时间字符串

代码任务 (90 mins)

1

环境准备

确保虚拟环境已激活,并安装必要的包:
# 确保虚拟环境已激活
source .venv/bin/activate

# 安装 email-validator(用于 EmailStr 类型)
pip install email-validator

# FastAPI 和 Pydantic 应该已经安装(Day 15)
# 如果没有,执行:pip install fastapi
2

任务 A:定义严格的用户模型

新建 schemas.py 文件,定义数据模型。任务分解
  1. 定义请求模型(UserCreate)- 用于创建用户
  2. 定义响应模型(UserResponse)- 用于返回用户信息
  3. 使用 Field 添加验证规则
#!/usr/bin/env python3
"""
Day 16 - Pydantic Schema 定义
演示如何定义严格的数据验证模型
"""

from pydantic import BaseModel, EmailStr, Field
from typing import Optional

# ========== 1. 请求模型 (UserCreate) ==========
# 这是前端用来注册时发送的数据结构
# 注意:这个模型包含密码字段,用于接收数据
class UserCreate(BaseModel):
    """
    创建用户的请求模型
    用于验证前端发送的用户注册数据
    """
    # username: 必填,字符串,长度 3-20
    # Field(...) 中的 ... 表示必填字段(Ellipsis)
    # min_length=3: 最小长度 3 个字符
    # max_length=20: 最大长度 20 个字符
    # description: 字段描述(会显示在 Swagger 文档中)
    username: str = Field(
        ..., 
        min_length=3, 
        max_length=20, 
        description="用户名,3-20 个字符"
    )
    
    # password: 必填,最少 8 位
    # 注意:密码不应该在响应中返回,所以响应模型中不包含此字段
    password: str = Field(
        ..., 
        min_length=8, 
        description="密码,至少 8 个字符(不会在响应中返回)"
    )
    
    # email: 必填,必须符合邮箱格式
    # EmailStr 是 Pydantic 提供的特殊类型,自动验证邮箱格式
    # 需要安装 email-validator: pip install email-validator
    email: EmailStr = Field(..., description="邮箱地址")
    
    # age: 选填 (Optional),默认 None
    # 但如果填了,必须在 0-120 之间
    # gt=0: 大于 0(greater than)
    # lt=120: 小于 120(less than)
    age: Optional[int] = Field(
        None, 
        gt=0, 
        lt=120, 
        description="年龄,0-120 之间(可选)"
    )

# ========== 2. 响应模型 (UserResponse) ==========
# 这是我们返回给前端的数据结构
# 注意:这里故意【不包含】password 字段,防止密码泄露!
class UserResponse(BaseModel):
    """
    用户响应模型
    用于返回用户信息,不包含敏感字段(如密码)
    """
    id: int = Field(..., description="用户 ID")
    username: str = Field(..., description="用户名")
    email: EmailStr = Field(..., description="邮箱地址")
    age: Optional[int] = Field(None, description="年龄")
    
    # Config 类的作用是让 Pydantic 支持读取 ORM 对象
    # from_attributes = True: 允许从对象属性创建模型(后面数据库部分会用到)
    class Config:
        from_attributes = True
代码解释
  • Field(...)... 表示必填字段
  • Field(None, ...)None 是默认值,表示可选字段
  • min_length:字符串最小长度验证
  • gtlt:数值范围验证
  • EmailStr:自动验证邮箱格式
  • Optional[int]:可选字段,类型为 int 或 None
验证步骤
  1. 检查文件语法是否正确
  2. 尝试导入:python -c "from schemas import UserCreate, UserResponse; print('OK')"
3

任务 B:集成到 API 路由

修改 16_main.py,使用刚才定义的模型。
#!/usr/bin/env python3
"""
Day 16 - Pydantic 数据验证实战
演示如何在 FastAPI 中使用 Pydantic 模型进行数据验证
"""

from fastapi import FastAPI, HTTPException
from schemas import UserCreate, UserResponse

# 初始化 FastAPI 应用
app = FastAPI(
    title="User Management API",
    description="演示 Pydantic 数据验证的 API",
    version="1.0.0"
)

# 模拟数据库(实际应该用真实数据库)
fake_users_db = []
next_user_id = 1

# ========== 关键点 1: user: UserCreate ==========
# 告诉 FastAPI,要用 UserCreate 这个模型去校验 request body
# 如果请求数据不符合 UserCreate 的定义,FastAPI 会自动返回 422 错误
#
# ========== 关键点 2: response_model=UserResponse ==========
# 告诉 FastAPI,返回数据时,请自动过滤掉不在 UserResponse 里的字段
# 比如即使数据库返回了 password,也会被自动过滤掉
@app.post("/users/", response_model=UserResponse, status_code=201)
def create_user(user: UserCreate):
    """
    创建用户接口
    
    功能:
    - **自动校验**: 长度、类型、邮箱格式
    - **自动脱敏**: 返回结果不含密码
    
    参数:
        user: 用户创建数据(自动验证)
    
    返回:
        UserResponse: 用户信息(不含密码)
    """
    # 如果代码能执行到这里,说明数据绝对是合法的!
    # 无需再做 if 判断,Pydantic 已经帮我们验证过了
    
    # 检查用户名是否已存在(业务逻辑验证)
    for existing_user in fake_users_db:
        if existing_user["username"] == user.username:
            raise HTTPException(
                status_code=400, 
                detail="Username already exists"
            )
    
    # 模拟保存到数据库
    global next_user_id
    new_user = {
        "id": next_user_id,
        "username": user.username,
        "email": user.email,
        "age": user.age,
        "password": user.password  # 这个字段会被 response_model 自动过滤
    }
    fake_users_db.append(new_user)
    next_user_id += 1
    
    print(f"✅ User created: {user.username} (password will not be returned)")
    
    # 返回用户信息
    # 注意:即使 new_user 包含 password,response_model 也会自动过滤掉
    return new_user

# ========== 查询用户列表 ==========
@app.get("/users/", response_model=list[UserResponse])
def list_users():
    """
    获取用户列表
    
    返回:
        用户列表(不含密码)
    """
    return fake_users_db

# ========== 查询单个用户 ==========
@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int):
    """
    获取单个用户信息
    
    参数:
        user_id: 用户 ID
    
    返回:
        UserResponse: 用户信息(不含密码)
    """
    for user in fake_users_db:
        if user["id"] == user_id:
            return user
    
    raise HTTPException(status_code=404, detail="User not found")
代码解释
  • user: UserCreate:FastAPI 自动验证请求体
  • response_model=UserResponse:自动过滤响应中的字段
  • status_code=201:创建成功返回 201(Created)
  • HTTPException:抛出 HTTP 异常,FastAPI 自动转换为错误响应
运行脚本
# 启动服务器
uvicorn 16_main:app --reload
验证步骤
  1. 访问 http://localhost:8000/docs 打开 Swagger UI
  2. 测试创建用户接口:
    • 点击 POST /users/
    • 点击 “Try it out”
    • 输入正确的数据,执行,应该返回 201
    • 输入错误的数据(如密码太短),执行,应该返回 422 错误
  3. 验证响应模型:
    • 创建用户后,查看返回的数据
    • 确认返回的数据中不包含 password 字段
常见错误
  • 422 Validation Error - 请求数据不符合 Schema 定义,检查字段类型和验证规则
  • ImportError: cannot import name 'EmailStr' - 未安装 email-validator,执行 pip install email-validator
  • FieldInfo(...) 错误 - Field 使用错误,检查语法
4

任务 C:测试数据验证

使用 Swagger UI 测试各种错误情况,观察 FastAPI 如何自动验证。测试用例
  1. 密码太短
    {
      "username": "admin",
      "password": "123",
      "email": "admin@example.com"
    }
    
    • 预期:422 Error,错误信息:ensure this value has at least 8 characters
  2. 邮箱格式错误
    {
      "username": "admin",
      "password": "secret_password",
      "email": "not_an_email"
    }
    
    • 预期:422 Error,错误信息:value is not a valid email address
  3. 年龄越界
    {
      "username": "admin",
      "password": "secret_password",
      "email": "admin@example.com",
      "age": 200
    }
    
    • 预期:422 Error,错误信息:ensure this value is less than 120
  4. 缺少必填字段
    {
      "username": "admin"
    }
    
    • 预期:422 Error,错误信息:field required(password 和 email)
  5. 正确的数据
    {
      "username": "admin",
      "password": "secret_password",
      "email": "admin@example.com",
      "age": 25
    }
    
    • 预期:201 Created,返回用户信息(不含密码)
这就是声明式编程的魅力:你只负责立规矩,FastAPI 负责当警察。

拓展任务 (30 mins)

挑战 1:自定义验证器

任务:使用 @validator 装饰器添加自定义验证逻辑。例如:验证用户名不能包含特殊字符。提示
from pydantic import validator

@validator('username')
def validate_username(cls, v):
    if not v.isalnum():
        raise ValueError('username must be alphanumeric')
    return v

挑战 2:嵌套模型

任务:创建一个包含嵌套模型的 Schema。例如:用户模型包含地址信息(地址也是一个模型)。提示
class Address(BaseModel):
    street: str
    city: str

class UserCreate(BaseModel):
    username: str
    address: Address  # 嵌套模型

今日产出物

  • schemas.py - Pydantic 数据模型定义
  • 16_main.py - 使用 Pydantic 的 FastAPI 应用

参考代码

查看参考代码

在 GitHub 查看完整的示例代码

在线运行

使用在线编辑器测试代码

实际应用场景

Pydantic 在 API 开发中的应用

  • 请求验证:自动验证前端发送的数据格式
  • 响应过滤:自动过滤敏感字段(如密码)
  • 类型转换:自动将字符串转换为整数、日期等
  • 文档生成:自动生成 API 文档,显示字段说明
  • 数据序列化:自动将 Python 对象转换为 JSON

Schema 设计最佳实践

  • 分离请求和响应模型:请求模型包含所有字段,响应模型排除敏感字段
  • 使用 Field 添加规则:利用 min_length、gt、lt 等规则增强验证
  • 提供默认值:为可选字段设置合理的默认值
  • 添加描述:使用 description 参数,便于生成文档
  • 使用特殊类型:EmailStr、IPvAnyAddress 等,减少手动验证代码
与 Day 17 的关联:今天学习的 Pydantic 数据验证,明天会学习依赖注入和中间件,将验证逻辑封装成可复用的依赖函数。

上一天: 快速入门

Day 15 | FastAPI 快速入门

下一天: 依赖注入

Day 17 | 依赖注入与中间件