FastAPI 进阶实战:请求体、文件上传、响应模型与数据校验
在前一篇 FastAPI 入门中,我们掌握了路由、参数、异常和异步基础。本文将深入进阶功能:Pydantic 模型嵌套与自定义校验、文件上传与保存、表单参数、响应模型过滤敏感字段,以及如何组织代码。通过完整的用户注册接口实战,带你写出生产级 API。
目录
- 代码组织最佳实践
- [请求体:Pydantic 模型详解](#请求体:Pydantic 模型详解)
- 2.1 定义请求体模型
- 2.2 自动解析 JSON
- 响应模型:过滤与格式化
- 文件上传与保存
- 4.1 单文件上传
- 4.2 多文件上传
- 4.3 文件保存技巧(MD5 重命名、扩展名提取)
- [表单参数:Form 处理](#表单参数:Form 处理)
- 混合表单与文件
- [Pydantic 进阶:Field 参数与自定义校验](#Pydantic 进阶:Field 参数与自定义校验)
- 7.1 Field 常用参数
- 7.2 自定义校验器
@field_validator - 7.3 模型嵌套与 List 类型
- 综合实战:用户注册接口(含头像上传)
- 总结与扩展
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-urlencoded 或 multipart/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。