# Pydantic v2 数据验证实战

> 参考完整规范: `@/Python项目开发准则.md#11-数据验证规范`

## 快速开始

```python
from pydantic import BaseModel, Field, field_validator

class User(BaseModel):
    """用户模型"""
    name: str = Field(..., min_length=1, max_length=100)
    email: str = Field(..., pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')
    age: int = Field(..., ge=0, le=150)

# 使用
user = User(name="John", email="john@example.com", age=30)
print(user)
```

## 常见验证需求

### 1. 长度/范围限制
```python
from pydantic import BaseModel, Field

class Product(BaseModel):
    name: str = Field(..., min_length=1, max_length=200)
    price: float = Field(..., gt=0, lt=1000000)  # gt: 大于, lt: 小于
    stock: int = Field(..., ge=0)  # ge: 大于等于
    tags: list[str] = Field(default_factory=list, max_length=10)
```

### 2. 正则表达式验证
```python
class Contact(BaseModel):
    phone: str = Field(..., pattern=r'^\+?1?\d{9,15}$')
    postal_code: str = Field(..., pattern=r'^\d{5}(-\d{4})?$')
```

### 3. 自定义验证器
```python
from pydantic import field_validator

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

    @field_validator('username')
    @classmethod
    def username_alphanumeric(cls, v: str) -> str:
        """用户名只能包含字母和数字"""
        if not v.isalnum():
            raise ValueError('用户名只能是字母和数字')
        return v

    @field_validator('password')
    @classmethod
    def password_strong(cls, v: str) -> str:
        """密码至少 8 个字符，包含字母和数字"""
        if len(v) < 8:
            raise ValueError('密码至少 8 个字符')
        if not any(c.isdigit() for c in v):
            raise ValueError('密码必须包含数字')
        return v
```

### 4. 嵌套模型
```python
class Address(BaseModel):
    street: str
    city: str
    country: str

class User(BaseModel):
    name: str
    email: str
    address: Address  # 自动验证嵌套模型

# 使用
data = {
    "name": "John",
    "email": "john@example.com",
    "address": {
        "street": "123 Main St",
        "city": "New York",
        "country": "USA"
    }
}
user = User(**data)
```

### 5. 模型验证（多字段约束）
```python
from pydantic import model_validator

class DateRange(BaseModel):
    start_date: str
    end_date: str

    @model_validator(mode='after')
    def end_after_start(self) -> 'DateRange':
        """确保结束日期晚于开始日期"""
        if self.start_date >= self.end_date:
            raise ValueError('结束日期必须晚于开始日期')
        return self
```

### 6. 条件验证
```python
from typing import Literal

class Account(BaseModel):
    account_type: Literal['personal', 'business']
    tax_id: str | None = None

    @field_validator('tax_id')
    @classmethod
    def validate_tax_id(cls, v: str | None, info) -> str | None:
        """商业账户必须有税号"""
        if info.data.get('account_type') == 'business' and not v:
            raise ValueError('商业账户必须提供税号')
        return v
```

## FastAPI 集成

```python
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class UserCreate(BaseModel):
    """创建用户请求"""
    name: str
    email: str
    age: int

@app.post("/users")
async def create_user(user: UserCreate):
    """创建用户 - FastAPI 自动验证请求体"""
    # 若验证失败，自动返回 422 和详细错误
    return {"status": "success", "user": user}

# 使用示例
# POST /users
# {"name": "John", "email": "john@example.com", "age": 30}
```

## 错误处理

```python
from pydantic import ValidationError

def safe_validate(data: dict, model_class) -> tuple[bool, dict | None]:
    """安全的验证"""
    try:
        instance = model_class(**data)
        return True, instance
    except ValidationError as e:
        errors = {
            error['loc'][0]: error['msg']
            for error in e.errors()
        }
        return False, errors

# 使用
success, result = safe_validate(
    {"name": "", "email": "invalid"},
    User
)
if not success:
    print(f"验证失败: {result}")  # {'name': 'ensure this value has at least 1 characters', ...}
```

## 常见场景

### 场景 1: API 请求验证
```python
class CreatePostRequest(BaseModel):
    title: str = Field(..., min_length=5, max_length=200)
    content: str = Field(..., min_length=10)
    tags: list[str] = Field(default_factory=list, max_length=5)
    status: Literal['draft', 'published'] = 'draft'
```

### 场景 2: 配置文件验证
```python
from pathlib import Path
from typing import Optional

class AppConfig(BaseModel):
    app_name: str
    debug: bool = False
    db_url: str = Field(..., pattern=r'^(postgres|mysql|sqlite):')
    log_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR'] = 'INFO'
    max_workers: int = Field(..., ge=1, le=100)
    secret_key: Optional[str] = None  # 从 .env 注入
```

### 场景 3: 动态验证
```python
class Order(BaseModel):
    items: list[dict]
    total: float

    @field_validator('items')
    @classmethod
    def items_not_empty(cls, v: list) -> list:
        if not v:
            raise ValueError('订单必须包含至少一项')
        return v

    @field_validator('total')
    @classmethod
    def total_positive(cls, v: float) -> float:
        if v <= 0:
            raise ValueError('总金额必须大于 0')
        return v
```

## 最佳实践

✅ **推荐：**
- 使用 `Field()` 定义约束，而不是在代码中硬编码
- 实现自定义验证器处理业务逻辑
- 为验证失败返回清晰的错误信息
- 在 API 入口层验证数据

❌ **避免：**
- 在业务逻辑层重复验证
- 忽略验证错误
- 硬编码正则表达式或约束值

## 参考

- [Pydantic 官方文档](https://docs.pydantic.dev/)
- 完整规范：`@/Python项目开发准则.md#11-数据验证规范`
