错误处理与数据校验
写给初学者:每个概念都从”为什么需要它”开始讲,配合通俗比喻和完整可运行代码。
一、Pydantic 数据校验
1.1 为什么需要数据校验?
想象一个注册接口,用户发来这样的 JSON:
{"username": "a", "age": -5, "email": "not-an-email"}
如果没有校验,这条垃圾数据就直接存进数据库了。Pydantic 的作用就是在数据进入你的函数之前,自动帮你检查数据合不合法。
1.2 BaseModel —— 一切的基础
BaseModel 是 Pydantic 的核心类。你定义一个类继承它,这个类就变成了一个”数据模板”。
from pydantic import BaseModel
class User(BaseModel):
name: str # 必须是字符串
age: int # 必须是整数
email: str # 必须是字符串
发生了什么? 当你用这个类去接收数据时,Pydantic 会自动:
-
检查
name是不是字符串 → 不是就报错 -
检查
age是不是整数 → 不是就报错 -
检查
email是不是字符串 → 不是就报错 -
如果漏了字段 → 也会报错
# ✅ 合法
user = User(name="张三", age=25, email="zhangsan@qq.com")
# ❌ 报错!age 应该是 int,你给了字符串
user = User(name="张三", age="不是数字", email="zhangsan@qq.com")
# ❌ 报错!少了 email 字段
user = User(name="张三", age=25)
# 🎁 额外福利:类型可以自动转换!
user = User(name="张三", age="25", email="zhangsan@qq.com")
# age 传了字符串 "25",Pydantic 自动转成整数 25,不会报错
1.3 Field —— 给字段加更多约束
光有类型检查不够用。比如用户名至少 3 个字符、年龄必须大于 0,这就需要 Field()。
from pydantic import BaseModel, Field
class User(BaseModel):
# ... 表示这个字段是必填的(没有默认值)
# min_length=3 表示最少 3 个字符
name: str = Field(..., min_length=3, max_length=20)
# gt=0 表示 greater than 0(大于 0)
# le=150 表示 less than or equal to 150(小于等于 150)
age: int = Field(..., gt=0, le=150)
# 默认值:如果不传 email,就用 "未填写"
email: str = Field(default="未填写")
1.4 field_validator —— 自定义校验规则
有时候内置的约束不够用,比如”邮箱必须包含 @”、“密码必须包含数字”,这就需要自定义校验器。
from pydantic import BaseModel, Field, field_validator
class User(BaseModel):
name: str = Field(..., min_length=3)
email: str
password: str = Field(..., min_length=8)
# @field_validator("email") 表示:这个函数用来校验 email 字段
# 参数 v 就是用户传进来的 email 的值
# 如果校验不通过,raise ValueError("错误信息")
# 如果校验通过,return v(可以顺便做转换,比如转小写)
@field_validator("email")
@classmethod
def check_email(cls, v: str) -> str:
# cls 指的是这个类本身(User),不用管它,固定写法
# v 是用户传入的 email 值
if "@" not in v:
raise ValueError("邮箱必须包含 @ 符号")
return v.lower() # 校验通过,顺便把邮箱转成小写
@field_validator("password")
@classmethod
def check_password(cls, v: str) -> str:
if not any(char.isdigit() for char in v):
raise ValueError("密码必须包含至少一个数字")
return v
运行效果:
# ✅ 合法,email 自动转小写
user = User(name="张三", email="ZhangSan@QQ.COM", password="abc12345")
print(user.email) # "zhangsan@qq.com"
# ❌ 报错:邮箱没有 @
user = User(name="张三", email="zhangsan.com", password="abc12345")
# ❌ 报错:密码没有数字
user = User(name="张三", email="a@b.com", password="abcdefgh")
# → ValidationError: 密码必须包含至少一个数字
1.5 常见错误(踩坑记录)
学 Pydantic 最容易踩的几个坑,用一个”错误代码 → 修正”的方式整理:
坑 1:Field 约束类型用错
# ❌ 错误:给字符串字段加了数值约束
email: str = Field(..., gt=0, le=150)
# ✅ 正确:字符串用 min_length / max_length
email: str = Field(..., min_length=5, max_length=150)
为什么错? gt、ge、lt、le 是给数字用的(大于、大于等于、小于、小于等于)。字符串长度要用 min_length 和 max_length。
坑 2:忘了给必填字段加约束
# ❌ 错误:age 没有任何约束,负数也能通过
age: int
# ✅ 正确:加个合理范围
age: int = Field(..., gt=0, le=150)
为什么错? 没有约束 = 只检查类型。age = -999 也会通过校验,因为它确实是 int。
速查:Field 约束该用哪个?
| 数据类型 | 约束类型 | 例子 |
|---|---|---|
| 字符串长度 | min_length / max_length | Field(min_length=3, max_length=20) |
| 数字大小 | gt / ge / lt / le | Field(gt=0, le=150) |
| 必填 | ... 或不给 default | Field(...) |
二、错误处理(Exception Handling)
2.1 为什么需要错误处理?
假设用户访问 /users/999,但用户 999 不存在。你不能什么都不说,也不能返回一个丑陋的 500 错误。你需要返回一个友好的、结构化的错误信息。
2.2 HTTPException —— 最常用的错误处理
from fastapi import FastAPI, HTTPException
app = FastAPI()
fake_users = {
1: {"id": 1, "name": "张三"},
2: {"id": 2, "name": "李四"},
}
@app.get("/users/{user_id}")
async def get_user(user_id: int):
# 如果用户不存在,抛出 HTTPException
if user_id not in fake_users:
raise HTTPException(
status_code=404, # 状态码
detail=f"用户 {user_id} 不存在" # 错误详情
)
# 如果用户存在,正常返回
return fake_users[user_id]
客户端收到的错误响应:
{
"detail": "用户 999 不存在"
}
HTTPException 的参数:
raise HTTPException(
status_code=404, # 必填:状态码
detail="用户不存在", # 必填:错误描述
headers={"X-Error-Code": "U001"} # 可选:自定义响应头
)
2.3 不同场景用不同的状态码
@app.post("/login")
async def login(username: str, password: str):
# 1. 用户不存在 → 404
if username not in db:
raise HTTPException(status_code=404, detail="用户不存在")
# 2. 密码错误 → 401(Unauthorized,未授权)
if db[username]["password"] != password:
raise HTTPException(status_code=401, detail="密码错误")
# 3. 账号被封 → 403(Forbidden,禁止访问)
if db[username]["banned"]:
raise HTTPException(status_code=403, detail="账号已被封禁")
# 4. 全部通过 → 200
return {"msg": "登录成功", "token": "xxx"}
2.4 自定义异常类 —— 处理业务逻辑错误
当你的业务逻辑比较复杂(比如支付、库存扣减),可以定义自己的异常类,让代码更清晰。
步骤:① 定义异常类 → ② 注册处理器 → ③ 在业务代码里抛出
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
# 继承 Exception,就像定义一个普通的 Python 类
class InsufficientBalance(Exception):
"""余额不足异常"""
def __init__(self, balance: float, required: float):
self.balance = balance # 当前余额
self.required = required # 需要的金额
class OutOfStock(Exception):
"""库存不足异常"""
def __init__(self, product: str, available: int, requested: int):
self.product = product
self.available = available
self.requested = requested
# 告诉 FastAPI:"当 InsufficientBalance 异常发生时,用这个函数处理"
@app.exception_handler(InsufficientBalance)
async def handle_insufficient_balance(request: Request, exc: InsufficientBalance):
return JSONResponse(
status_code=400, # 返回 400 错误
content={
"error": "余额不足",
"your_balance": exc.balance,
"required": exc.required,
"shortage": round(exc.required - exc.balance, 2)
}
)
@app.exception_handler(OutOfStock)
async def handle_out_of_stock(request: Request, exc: OutOfStock):
return JSONResponse(
status_code=400,
content={
"error": "库存不足",
"product": exc.product,
"available": exc.available,
"requested": exc.requested
}
)
# ========== 第三步:在业务代码里抛出异常 ==========
my_balance = 50.0
@app.post("/buy/{product}")
async def buy_product(product: str, quantity: int = 1):
prices = {"apple": 30.0, "banana": 20.0}
stock = {"apple": 10, "banana": 0}
if product not in prices:
raise HTTPException(status_code=404, detail="商品不存在")
total_price = prices[product] * quantity
# 检查库存(抛出自定义异常)
if stock[product] < quantity:
raise OutOfStock(product=product, available=stock[product], requested=quantity)
# 检查余额(抛出自定义异常)
if my_balance < total_price:
raise InsufficientBalance(balance=my_balance, required=total_price)
return {"msg": f"购买 {quantity} 个 {product} 成功"}
为什么要用自定义异常而不是直接用 HTTPException?
# ❌ 用 HTTPException:错误信息散落在业务代码里,不好维护
if my_balance < total_price:
raise HTTPException(status_code=400, detail="余额不足")
# 错误格式、状态码等由异常处理器统一管理
if my_balance < total_price:
raise InsufficientBalance(balance=my_balance, required=total_price)
2.5 全局异常处理器 —— 兜底方案
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""
捕获所有未被其他处理器捕获的异常
相当于 try-except 的"最后防线"
防止服务器返回难看的默认 500 错误
"""
print(f"未预期的错误: {type(exc).__name__}: {exc}") # 打印日志方便排查
return JSONResponse(
status_code=500,
content={
"error": "服务器内部错误",
"detail": "请联系管理员"
}
)
2.6 自定义 Pydantic 校验错误格式(进阶)
FastAPI 默认的 422 错误格式比较长,你可以自定义成更简洁的格式:
from fastapi.exceptions import RequestValidationError
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
# 把 FastAPI 默认的长格式简化
errors = []
for error in exc.errors():
field = " -> ".join(str(loc) for loc in error["loc"])
errors.append(f"{field}: {error['msg']}")
return JSONResponse(
status_code=422,
content={"error": "数据校验失败", "details": errors}
)
速查表
| 概念 | 一句话解释 | 关键代码 |
|---|---|---|
| Pydantic BaseModel | 定义数据结构,自动校验类型 | class User(BaseModel): ... |
| Field | 给字段加约束(长度、范围) | name: str = Field(..., min_length=3) |
| field_validator | 自定义校验规则 | @field_validator("email") |
| HTTPException | 抛出 HTTP 错误(最常用) | raise HTTPException(status_code=404, detail="...") |
| 自定义异常 | 业务逻辑异常,统一格式处理 | class MyError(Exception): ... |
| exception_handler | 注册异常处理器 | @app.exception_handler(MyError) |
| 全局异常处理 | 兜底,防止裸 500 | @app.exception_handler(Exception) |
▶ 对应原理:09-全局异常处理
速记卡(面试闪卡)
Q1:一句话讲清「错误处理与数据校验」到底是什么?
A:FastAPI 用 Pydantic 在进函数前校验数据,用 HTTPException/异常处理器返回友好错误。
Q2:一、Pydantic 数据校验 —— 怎么理解?
A:BaseModel 是数据模板,收到数据自动查类型、查必填;Field 加约束(min_length、gt/le);field_validator 写自定义规则(邮箱要带@)。就像进门安检——垃圾数据(age=-5、邮箱乱写)根本进不了你的函数。
Q3:二、HTTPException 错误处理 —— 怎么理解?
A:用户不存在就 raise HTTPException(status_code=404, detail=…),客户端拿到结构化 {“detail”:…} 而非丑陋的 500。不同场景用不同码:404 不存在、401 密码错、403 被封、200 成功,语义清晰。
Q4:三、自定义异常与处理器 —— 怎么理解?
A:支付/库存这类复杂逻辑,定义自己的异常类(如 InsufficientBalance),用 @app.exception_handler 注册统一格式的处理器。业务代码只扔”余额不足”这个事实,错误长什么样、什么状态码由处理器统一管。
Q5:四、全局兜底与校验美化 —— 怎么理解?
A:@app.exception_handler(Exception) 是最后防线,兜住所有漏网的异常,防止裸 500 吓到用户;还可重写 RequestValidationError,把默认 422 的长格式压成简洁的 {“error”,“details”},前端好解析。
Q6:核心速记主线有哪些?
-
Pydantic BaseModel 自动校验类型与必填
-
Field 加长度/范围约束,field_validator 写自定义规则
-
HTTPException 抛友好错误(404/401/403)
-
自定义异常 + 处理器统一错误格式
-
全局处理器兜底防裸 500
口诀
A:Pydantic 先把数据验,
Field 约束规则定底线;
异常抛出处理器管,
HTTPException 礼貌相还。
相关链接
-
目录:00-FastAPI
-
上一篇:04-响应模型与状态码
-
下一篇:06-依赖注入