> ## Documentation Index
> Fetch the complete documentation index at: https://docs.akria.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Day 16 | Pydantic 数据验证

> 如何让你的 API 像安检员一样严格：Schema 定义与自动校验

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

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="book" size={26} color="#059669" /> 学习内容 (30 mins)</span>

在开始写代码前，先搞懂这些核心概念，否则后面的代码你会看得云里雾里。

<AccordionGroup>
  <Accordion title="为什么需要 Pydantic？(15 mins)" icon="shield">
    **什么是 Pydantic？**

    Pydantic 是一个数据验证库，使用 Python 类型提示来验证数据。它是 FastAPI 的核心依赖，负责验证请求和响应数据。

    **为什么需要数据验证？**

    * **安全性**：防止恶意数据注入
    * **数据完整性**：确保数据格式正确
    * **开发效率**：自动验证，减少手动检查代码
    * **文档生成**：自动生成 API 文档

    **传统方式 vs Pydantic**：

    **传统方式（Flask/Django）**：

    ```python theme={null}
    # 需要手动检查每个字段
    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）**：

    ```python theme={null}
    # 定义一个模型，自动验证
    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 支持好
  </Accordion>

  <Accordion title="Pydantic 核心概念 (15 mins)" icon="code">
    **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`**：自动解析日期时间字符串
  </Accordion>
</AccordionGroup>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="code" size={26} color="#059669" /> 代码任务 (90 mins)</span>

<Steps>
  <Step title="环境准备">
    确保虚拟环境已激活，并安装必要的包：

    ```bash theme={null}
    # 确保虚拟环境已激活
    source .venv/bin/activate

    # 安装 email-validator（用于 EmailStr 类型）
    pip install email-validator

    # FastAPI 和 Pydantic 应该已经安装（Day 15）
    # 如果没有，执行：pip install fastapi
    ```
  </Step>

  <Step title="任务 A：定义严格的用户模型">
    新建 `schemas.py` 文件，定义数据模型。

    **任务分解**：

    1. 定义请求模型（UserCreate）- 用于创建用户
    2. 定义响应模型（UserResponse）- 用于返回用户信息
    3. 使用 Field 添加验证规则

    <CodeGroup>
      ```python schemas.py theme={null}
      #!/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
      ```
    </CodeGroup>

    **代码解释**：

    * **`Field(...)`**：`...` 表示必填字段
    * **`Field(None, ...)`**：`None` 是默认值，表示可选字段
    * **`min_length`**：字符串最小长度验证
    * **`gt`、`lt`**：数值范围验证
    * **`EmailStr`**：自动验证邮箱格式
    * **`Optional[int]`**：可选字段，类型为 int 或 None

    **验证步骤**：

    1. 检查文件语法是否正确
    2. 尝试导入：`python -c "from schemas import UserCreate, UserResponse; print('OK')"`
  </Step>

  <Step title="任务 B：集成到 API 路由">
    修改 `16_main.py`，使用刚才定义的模型。

    <CodeGroup>
      ```python 16_main.py theme={null}
      #!/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")
      ```
    </CodeGroup>

    **代码解释**：

    * **`user: UserCreate`**：FastAPI 自动验证请求体
    * **`response_model=UserResponse`**：自动过滤响应中的字段
    * **`status_code=201`**：创建成功返回 201（Created）
    * **`HTTPException`**：抛出 HTTP 异常，FastAPI 自动转换为错误响应

    **运行脚本**：

    ```bash theme={null}
    # 启动服务器
    uvicorn 16_main:app --reload
    ```

    **验证步骤**：

    1. 访问 `http://localhost:8000/docs` 打开 Swagger UI
    2. 测试创建用户接口：
       * 点击 `POST /users/`
       * 点击 "Try it out"
       * 输入正确的数据，执行，应该返回 201
       * 输入错误的数据（如密码太短），执行，应该返回 422 错误
    3. 验证响应模型：
       * 创建用户后，查看返回的数据
       * 确认返回的数据中**不包含** `password` 字段

    **常见错误**：

    <Warning>
      * ❌ `422 Validation Error` - 请求数据不符合 Schema 定义，检查字段类型和验证规则
      * ❌ `ImportError: cannot import name 'EmailStr'` - 未安装 email-validator，执行 `pip install email-validator`
      * ❌ `FieldInfo(...)` 错误 - Field 使用错误，检查语法
    </Warning>
  </Step>

  <Step title="任务 C：测试数据验证">
    使用 Swagger UI 测试各种错误情况，观察 FastAPI 如何自动验证。

    **测试用例**：

    1. **密码太短**：
       ```json theme={null}
       {
         "username": "admin",
         "password": "123",
         "email": "admin@example.com"
       }
       ```
       * 预期：422 Error，错误信息：`ensure this value has at least 8 characters`
    2. **邮箱格式错误**：
       ```json theme={null}
       {
         "username": "admin",
         "password": "secret_password",
         "email": "not_an_email"
       }
       ```
       * 预期：422 Error，错误信息：`value is not a valid email address`
    3. **年龄越界**：
       ```json theme={null}
       {
         "username": "admin",
         "password": "secret_password",
         "email": "admin@example.com",
         "age": 200
       }
       ```
       * 预期：422 Error，错误信息：`ensure this value is less than 120`
    4. **缺少必填字段**：
       ```json theme={null}
       {
         "username": "admin"
       }
       ```
       * 预期：422 Error，错误信息：`field required`（password 和 email）
    5. **正确的数据**：
       ```json theme={null}
       {
         "username": "admin",
         "password": "secret_password",
         "email": "admin@example.com",
         "age": 25
       }
       ```
       * 预期：201 Created，返回用户信息（不含密码）

    **这就是声明式编程的魅力：你只负责立规矩，FastAPI 负责当警察。**
  </Step>
</Steps>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="lightbulb" size={26} color="#059669" /> 拓展任务 (30 mins)</span>

<CardGroup cols={2}>
  <Card title="挑战 1：自定义验证器" icon="code">
    **任务**：使用 `@validator` 装饰器添加自定义验证逻辑。

    例如：验证用户名不能包含特殊字符。

    **提示**：

    ```python theme={null}
    from pydantic import validator

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

  <Card title="挑战 2：嵌套模型" icon="database">
    **任务**：创建一个包含嵌套模型的 Schema。

    例如：用户模型包含地址信息（地址也是一个模型）。

    **提示**：

    ```python theme={null}
    class Address(BaseModel):
        street: str
        city: str

    class UserCreate(BaseModel):
        username: str
        address: Address  # 嵌套模型
    ```
  </Card>
</CardGroup>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="file" size={26} color="#059669" /> 今日产出物</span>

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

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="github" size={26} color="#059669" /> 参考代码</span>

<CardGroup cols={2}>
  <Card title="查看参考代码" icon="code" href="https://github.com/akriamail/insightful-ops/tree/main/docs/mintlify/scripts/training/30daystudy/week-3/day-16">
    在 GitHub 查看完整的示例代码

    <br />

    <small>包含 schemas.py 和 16\_main.py</small>
  </Card>

  <Card title="在线运行" icon="play" href="https://replit.com">
    使用在线编辑器测试代码

    <br />

    <small>无需本地环境配置</small>
  </Card>
</CardGroup>

***

## 实际应用场景

<CardGroup cols={2}>
  <Card title="Pydantic 在 API 开发中的应用" icon="server">
    * **请求验证**：自动验证前端发送的数据格式
    * **响应过滤**：自动过滤敏感字段（如密码）
    * **类型转换**：自动将字符串转换为整数、日期等
    * **文档生成**：自动生成 API 文档，显示字段说明
    * **数据序列化**：自动将 Python 对象转换为 JSON
  </Card>

  <Card title="Schema 设计最佳实践" icon="check">
    * **分离请求和响应模型**：请求模型包含所有字段，响应模型排除敏感字段
    * **使用 Field 添加规则**：利用 min\_length、gt、lt 等规则增强验证
    * **提供默认值**：为可选字段设置合理的默认值
    * **添加描述**：使用 description 参数，便于生成文档
    * **使用特殊类型**：EmailStr、IPvAnyAddress 等，减少手动验证代码
  </Card>
</CardGroup>

<Tip>
  **与 Day 17 的关联**：今天学习的 Pydantic 数据验证，明天会学习依赖注入和中间件，将验证逻辑封装成可复用的依赖函数。
</Tip>

***

<CardGroup cols={2}>
  <Card title="上一天: 快速入门" href="/training/30daystudy/day-15">
    Day 15 | FastAPI 快速入门
  </Card>

  <Card title="下一天: 依赖注入" href="/training/30daystudy/day-17">
    Day 17 | 依赖注入与中间件
  </Card>
</CardGroup>
