FastAPI 进阶实战:请求体、文件上传、响应模型与数据校验

FastAPI 进阶实战:请求体、文件上传、响应模型与数据校验

在前一篇 FastAPI 入门中,我们掌握了路由、参数、异常和异步基础。本文将深入进阶功能:Pydantic 模型嵌套与自定义校验、文件上传与保存、表单参数、响应模型过滤敏感字段,以及如何组织代码。通过完整的用户注册接口实战,带你写出生产级 API。


目录

  1. 代码组织最佳实践
  2. [请求体:Pydantic 模型详解](#请求体:Pydantic 模型详解)
    • 2.1 定义请求体模型
    • 2.2 自动解析 JSON
  3. 响应模型:过滤与格式化
  4. 文件上传与保存
    • 4.1 单文件上传
    • 4.2 多文件上传
    • 4.3 文件保存技巧(MD5 重命名、扩展名提取)
  5. [表单参数:Form 处理](#表单参数:Form 处理)
  6. 混合表单与文件
  7. [Pydantic 进阶:Field 参数与自定义校验](#Pydantic 进阶:Field 参数与自定义校验)
    • 7.1 Field 常用参数
    • 7.2 自定义校验器 @field_validator
    • 7.3 模型嵌套与 List 类型
  8. 综合实战:用户注册接口(含头像上传)
  9. 总结与扩展

1. 代码组织最佳实践

当项目变大时,合理的代码组织至关重要。推荐结构:

复制代码
myproject/
├── main.py               # 应用入口
├── models/               # Pydantic 模型
│   ├── user.py
│   └── ...
├── routers/              # 路由模块(APIRouter)
│   ├── user.py
│   └── ...
├── utils/                # 工具函数
│   ├── file_utils.py
│   └── ...
├── uploads/              # 上传文件存储目录
└── requirements.txt

快速实践

  • 使用 if __name__ == "__main__" 控制启动。
  • 抽取公共函数(如保存文件、查找用户)到 utils
  • 使用 response_model 保证输出格式一致。
  • 用 Pydantic 模型定义请求体和响应体,避免字典散落。

2. 请求体:Pydantic 模型详解

2.1 定义请求体模型

继承 BaseModel,声明字段及类型。

python 复制代码
from pydantic import BaseModel

class UserCreate(BaseModel):
    username: str
    email: str
    age: int

FastAPI 自动解析 JSON 请求体为模型实例。

2.2 自动解析 JSON 请求体

python 复制代码
from fastapi import FastAPI

app = FastAPI()

@app.post("/users")
def create_user(user: UserCreate):   # FastAPI 自动从 JSON 解析
    return {"username": user.username, "age": user.age}

客户端发送 {"username": "alice", "email": "a@b.com", "age": 25},自动校验类型并转换。


3. 响应模型:过滤与格式化

使用 response_model 参数可以:

  • 自动过滤未在模型中定义的字段(如密码)。
  • 保证输出格式一致。
  • 支持嵌套模型。
python 复制代码
class UserResponse(BaseModel):
    username: str
    email: str
    # 注意:不包含 password 字段

@app.post("/users", response_model=UserResponse)
def create_user(user: UserCreate):
    # 假设实际存储的 user 有 password 字段,但响应中不会返回
    db_user = {"username": user.username, "email": user.email, "password": "secret"}
    return db_user   # 自动过滤 password

也可以直接返回模型实例:return UserResponse(username=..., email=...) 或字典,FastAPI 会自动适配。


4. 文件上传与保存

4.1 单文件上传

使用 UploadFile 类型,配合 File()

python 复制代码
from fastapi import File, UploadFile

@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
    content = await file.read()
    return {"filename": file.filename, "size": len(content)}

4.2 多文件上传

python 复制代码
from typing import List

@app.post("/upload-multiple")
async def upload_multiple(files: List[UploadFile] = File(...)):
    for file in files:
        content = await file.read()
        # 处理每个文件
    return {"count": len(files)}

4.3 文件保存技巧(MD5 重命名、扩展名提取)

实际生产中需避免文件名冲突,通常使用 MD5 或 UUID 重命名。

python 复制代码
import os
import hashlib
import aiofiles

async def save_file(file: UploadFile, upload_dir: str = "uploads") -> str:
    # 1. 读取内容
    content = await file.read()
    # 2. 计算 MD5
    md5 = hashlib.md5(content).hexdigest()
    # 3. 获取扩展名
    ext = os.path.splitext(file.filename)[1]   # 如 ".png"
    # 4. 生成唯一文件名
    safe_filename = f"{md5}{ext}"
    file_path = os.path.join(upload_dir, safe_filename)
    # 5. 创建目录(如果不存在)
    os.makedirs(upload_dir, exist_ok=True)
    # 6. 写入文件
    async with aiofiles.open(file_path, "wb") as f:
        await f.write(content)
    return file_path

然后在路由中使用:

python 复制代码
@app.post("/upload-avatar")
async def upload_avatar(avatar: UploadFile = File(...)):
    path = await save_file(avatar)
    return {"path": path}

5. 表单参数:Form 处理

当客户端使用 application/x-www-form-urlencodedmultipart/form-data 提交表单数据(非 JSON)时,使用 Form()

python 复制代码
from fastapi import Form

@app.post("/login")
def login(username: str = Form(...), password: str = Form(...)):
    # 验证逻辑
    return {"username": username}

支持默认值和校验:

python 复制代码
username: str = Form(..., min_length=4, max_length=20)

6. 混合表单与文件

文本字段用 Form(),文件字段用 File(),可同时使用。

python 复制代码
@app.post("/register")
async def register(
    username: str = Form(...),
    age: int = Form(..., ge=18),
    avatar: UploadFile = File(...)
):
    avatar_path = await save_file(avatar)
    return {"username": username, "age": age, "avatar": avatar_path}

⚠️ 注意:此时客户端必须使用 multipart/form-data 编码,不能使用 JSON。


7. Pydantic 进阶:Field 参数与自定义校验

7.1 Field 常用参数

在模型字段中使用 Field 添加校验和描述。

参数 说明
default 默认值(若为必填则使用 ...
gt 大于(greater than)
ge 大于等于
lt 小于
le 小于等于
min_length 字符串最小长度
max_length 字符串最大长度
pattern 正则表达式
description 文档描述
python 复制代码
from pydantic import BaseModel, Field

class UserCreate(BaseModel):
    username: str = Field(..., min_length=3, max_length=20, description="用户名")
    age: int = Field(..., ge=0, le=150)
    email: str = Field(..., pattern=r"^\S+@\S+\.\S+$")

7.2 自定义校验器 @field_validator

使用装饰器对单个字段进行自定义校验,抛出 ValueError 即可。

python 复制代码
from pydantic import field_validator

class UserCreate(BaseModel):
    username: str
    password: str
    confirm_password: str

    @field_validator('confirm_password')
    @classmethod
    def passwords_match(cls, v, info):
        if v != info.data.get('password'):
            raise ValueError('两次密码不一致')
        return v

在 Pydantic v2 中,推荐使用 @field_validator(之前为 @validator)。可通过 values 参数访问其他字段。

7.3 模型嵌套与 List 类型

模型字段可以是另一个模型对象或列表。

python 复制代码
class Address(BaseModel):
    city: str
    street: str

class UserWithAddress(BaseModel):
    username: str
    addresses: List[Address]   # 嵌套模型列表

# 使用示例
data = {
    "username": "alice",
    "addresses": [
        {"city": "Beijing", "street": "Xizhimen"},
        {"city": "Shanghai", "street": "Nanjing Road"}
    ]
}
user = UserWithAddress(**data)

8. 综合实战:用户注册接口(含头像上传)

下面实现一个完整的用户注册接口,包含:

  • 请求体 JSON(用户名、邮箱、密码)
  • 表单文件(头像)
  • 响应模型(不返回密码)
  • 文件保存(MD5 重命名)
  • 自定义校验(密码强度、邮箱格式)
python 复制代码
import os
import hashlib
import re
from fastapi import FastAPI, File, UploadFile, Form, HTTPException
from pydantic import BaseModel, Field, field_validator
from typing import Optional
import aiofiles

app = FastAPI(title="用户注册API")

# ---------- 请求体模型 ----------
class UserRegisterRequest(BaseModel):
    username: str = Field(..., min_length=3, max_length=20)
    email: str = Field(..., pattern=r"^\S+@\S+\.\S+$")
    password: str = Field(..., min_length=6)

    @field_validator('password')
    @classmethod
    def strong_password(cls, v):
        if not re.search(r"\d", v):
            raise ValueError("密码必须包含数字")
        if not re.search(r"[A-Za-z]", v):
            raise ValueError("密码必须包含字母")
        return v

# ---------- 响应模型(不返回密码) ----------
class UserResponse(BaseModel):
    username: str
    email: str
    avatar_url: Optional[str] = None

# ---------- 文件保存工具 ----------
async def save_avatar(file: UploadFile) -> str:
    content = await file.read()
    # 仅允许图片
    if not file.content_type.startswith("image/"):
        raise HTTPException(400, "只允许上传图片")
    md5 = hashlib.md5(content).hexdigest()
    ext = os.path.splitext(file.filename)[1]
    safe_name = f"{md5}{ext}"
    upload_dir = "uploads/avatars"
    os.makedirs(upload_dir, exist_ok=True)
    file_path = os.path.join(upload_dir, safe_name)
    async with aiofiles.open(file_path, "wb") as f:
        await f.write(content)
    return f"/{file_path}"   # 模拟访问URL

# ---------- 注册接口 ----------
@app.post("/register", response_model=UserResponse)
async def register(
    user_data: UserRegisterRequest,   # JSON 请求体
    avatar: UploadFile = File(...)    # 文件字段
):
    # 模拟保存用户信息到数据库
    avatar_url = await save_avatar(avatar)
    # 返回响应(不包含密码)
    return UserResponse(
        username=user_data.username,
        email=user_data.email,
        avatar_url=avatar_url
    )

# ---------- 启动 ----------
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

测试方式

  • 使用 Postman 或 FastAPI 自动文档 /docs
  • 选择 multipart/form-data 方式:同时提供 JSON 字段(注意需要将 JSON 转为键值对,复杂结构建议分开为表单字段,或者将 JSON 序列化后放在一个字段中。更常见做法:将用户信息也拆成表单字段,但本例演示了混合 JSON+文件的高级用法,实际应用中需注意客户端需将 JSON 对象序列化后以字符串形式发送,后端再用 json.loads 解析。更推荐全部使用表单字段或全部使用 JSON(文件单独上传)。为简化,上述示例假定客户端能将 JSON 数据以表单字段方式发送(不推荐复杂 JSON)。更好的设计:用两个接口分开:先上传文件获得 URL,再提交 JSON。)

实际生产建议 :将文件上传和用户信息创建分为两步,或使用 multipart/form-data 的文本字段传递用户信息的 JSON 字符串。


9. 总结与扩展

本文涵盖了 FastAPI 进阶开发的核心内容:

功能 关键点
代码组织 分层结构、公共函数、启动控制
请求体 Pydantic 模型、自动 JSON 解析
响应模型 response_model 过滤敏感字段
文件上传 UploadFile + File()、异步读写、MD5 重命名
表单参数 Form() 处理 urlencoded/form-data
混合表单与文件 Form()File() 同时使用
Pydantic 校验 Field 参数、@field_validator、模型嵌套

下一步学习

  • 学习 ORM 框架(如 SQLAlchemy)与数据库集成,掌握增删改查、事务管理。
  • 理解 RESTful API 设计规范,合理使用 HTTP 方法、状态码、资源路径。
  • 掌握子路由拆分模块,避免单文件臃肿。
  • 使用依赖注入(Depends)抽离公共逻辑(分页、认证、数据库会话)。
  • 学习中间件统一处理请求与响应(日志、耗时、跨域)。
  • 配置 CORS 解决前后端分离的跨域问题,并学会挂载静态资源。

FastAPI 的高效与优雅,使其成为 Python Web 开发的首选。希望你通过本文,能写出更健壮、可维护的 API。

相关推荐
ZhengEnCi2 小时前
09a-斯坦福 CS336 作业一:BPE 分词器
python·神经网络
测试员周周2 小时前
【Appium 系列】第18节-重试与容错 — 移动端测试的稳定性保障
人工智能·python·功能测试·ui·单元测试·appium·测试用例
还是鼠鼠2 小时前
AI掘金头条新闻系统 (Toutiao News)-用户注册-创建用户
后端·python·mysql·fastapi·web
灰灰勇闯IT2 小时前
DeepSeek-R1 在 CANN 上的推理部署
pytorch·python·深度学习
天才测试猿4 小时前
Jenkins+Docker自动化测试全攻略
自动化测试·软件测试·python·测试工具·docker·jenkins·测试用例
5201-4 小时前
向量数据库在 NPU 上的加速
数据库·pytorch·python
arbitrary194 小时前
自动化业务通报系统实现
大数据·数据库·python·jupyter
yuhuofei20214 小时前
【Python入门】Python中字符串相关拓展
android·java·python
weixin199701080164 小时前
[特殊字符] 人工抓取数据革命:从“人肉爬虫”到“智能数据工厂”全面转型指南
开发语言·爬虫·python