JsonScheme与数据验证
bash
from pydantic import BaseModel, validator, Field
from typing import Optional, List
from enum import Enum
class UserRole(str, Enum):
ADMIN = "admin"
USER = "user"
MODERATOR = "moderator"
class UserCreate(BaseModel):
username: str = Field(..., min_length=3, max_length=50, description="用户名")
email: str = Field(..., regex=r'^[\w\.-]+@[\w\.-]+\.\w+$', description="邮箱地址")
password: str = Field(..., min_length=8, description="密码")
full_name: Optional[str] = Field(None, max_length=100, description="全名")
role: UserRole = Field(UserRole.USER, description="用户角色")
age: Optional[int] = Field(None, ge=0, le=150, description="年龄")
@validator('username')
def username_must_be_alphanumeric(cls, v):
if not v.isalnum():
raise ValueError('用户名只能包含字母和数字')
return v
@validator('password')
def password_strength(cls, v):
if not any(c.isupper() for c in v):
raise ValueError('密码必须包含至少一个大写字母')
if not any(c.islower() for c in v):
raise ValueError('密码必须包含至少一个小写字母')
if not any(c.isdigit() for c in v):
raise ValueError('密码必须包含至少一个数字')
return v
class Config:
# 生成 JSON Schema 示例
schema_extra = {
"example": {
"username": "johndoe",
"email": "john@example.com",
"password": "SecurePass123",
"full_name": "John Doe",
"role": "user",
"age": 30
}
}
# 使用自定义验证器
@app.post("/users", response_model=UserResponse)
async def create_user(user: UserCreate):
"""创建新用户,包含数据验证"""
# Pydantic 自动验证输入数据
return await create_new_user(user)
整体通俗解读
这是 FastAPI + Pydantic 请求入参校验模型:新增用户接口的入参结构体 。
前端POST传JSON,FastAPI自动用UserCreate做类型+字段规则+自定义业务校验 ,任意一项不满足直接抛出RequestValidationError,被你之前写的全局异常捕获,返回统一错误JSON。
逐个拆解知识点,分5块:枚举Enum、Field参数、Optional、@validator自定义校验、Config示例
一、UserRole(str, Enum) 枚举角色
python
class UserRole(str, Enum):
ADMIN = "admin"
USER = "user"
MODERATOR = "moderator"
- 继承
str、Enum:枚举值存字符串,前端只能传:admin/user/moderator 三者之一; role: UserRole = Field(UserRole.USER):不传参数默认user,前端传别的字符直接校验报错;
作用:固定可选值,防止非法角色传入,替代手写
@validator判断枚举范围。
二、各个字段 Field 内置校验(简单规则用Field)
python
username: str = Field(..., min_length=3, max_length=50, description="用户名")
email: str = Field(..., regex=r'^[\w\.-]+@[\w\.-]+\.\w+$', description="邮箱地址")
password: str = Field(..., min_length=8, description="密码")
full_name: Optional[str] = Field(None, max_length=100, description="全名")
role: UserRole = Field(UserRole.USER, description="用户角色")
age: Optional[int] = Field(None, ge=0, le=150, description="年龄")
关键字细节
Field(...):...代表必填字段,不能为空、必须传 ;
Field(None):配合Optional=非必填,前端可省略不传,默认None- 内置校验参数:
min_length/max_length:字符串长度限制regex:v1写法正则(v2推荐pattern),邮箱格式校验ge=0、le=150:数字范围:age≥0、age≤150
Optional[str] = Field(None)
复习你之前学的:
Optional=值可以是str/None;=None=字段可省略不传;
full_name、age都是选填。
三、@validator 自定义复杂校验(Field实现不了的规则)
简单长度、正则用Field;多条件复杂规则用@validator
1. username 校验:只能大小写+数字,不能有符号
python
@validator('username')
def username_must_be_alphanumeric(cls, v):
if not v.isalnum():
raise ValueError('用户名只能包含字母和数字')
return v
isalnum():纯字母数字返回True,包含_ @ -等符号直接抛异常。
2. password 强度校验(Field无法拆分大小写、数字判断)
要求:至少1大写 + 1小写 + 1数字
python
@validator('password')
def password_strength(cls, v):
if not any(c.isupper() for c in v): # 无大写
raise ValueError('密码必须包含至少一个大写字母')
if not any(c.islower() for c in v): # 无小写
raise ValueError('密码必须包含至少一个小写字母')
if not any(c.isdigit() for c in v): # 无数字
raise ValueError('密码必须包含至少一个数字')
return v
any(生成器):只要有一个满足条件就返回True。
执行顺序:字段类型转换 → Field规则校验 → @validator校验。任意一步报错 → 抛出
RequestValidationError→ 进入全局422异常处理器。
四、class Config:schema_extra = {"example":...}
python
class Config:
schema_extra = {
"example": {
"username": "johndoe",
"email": "john@example.com",
"password": "SecurePass123",
"full_name": "John Doe",
"role": "user",
"age": 30
}
}
作用:
- FastAPI自动生成接口文档(/docs)时,填入请求示例,前端调试接口一键填充样例JSON;
- Pydantic v2 改为
model_config={"json_schema_extra":{}},v1写法是Config内部schema_extra。
五、接口路由
python
@app.post("/users", response_model=UserResponse)
async def create_user(user: UserCreate):
user: UserCreate:FastAPI自动把POST的JSON请求体解析成UserCreate实例,自动全量校验;校验失败直接422,不会进入函数内部;response_model=UserResponse:出参格式化,接口返回数据自动按照UserResponse模型过滤字段、格式化。
六、完整校验触发场景汇总(面试高频)
- username少于3位/超50位 → Field报错
- username带中文、下划线 → @validator报错
- email格式错误 → regex正则报错
- 密码小于8位 → Field报错;密码缺大写/小写/数字 → @validator报错
- age传负数、160 → ge/le数值报错
- role传admin123非法字符 → 枚举Enum报错
- full_name不传、age不传:合法,自动为None(Optional+默认None)
七、精简总结(背诵)
- 固定可选值用Enum枚举(角色);
- 长度、范围、正则等简单规则用Field;
- 复杂多条件、自定义业务规则用@validator;
- 非必填字段:Optional类型 = Field(None);
- Config.example用于接口文档展示请求样例;
- 入参注解
user:UserCreate由FastAPI自动做全量校验,失败422。
错误响应格式
bash
class ErrorDetail(BaseModel):
field: str
message: str
code: str
class ErrorResponse(BaseModel):
error: str
message: str
details: Optional[List[ErrorDetail]] = None
timestamp: datetime = Field(default_factory=datetime.now)
# 自定义异常处理
from fastapi import Request
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = []
for error in exc.errors():
errors.append(ErrorDetail(
field='.'.join(str(x) for x in error['loc']),
message=error['msg'],
code=error['type']
))
return JSONResponse(
status_code=422,
content=ErrorResponse(
error="Validation Error",
message="请求数据验证失败",
details=errors
).dict()
)
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
return JSONResponse(
status_code=exc.status_code,
content=ErrorResponse(
error=f"HTTP {exc.status_code}",
message=exc.detail
).dict()
)
整体一句话概括
这段代码是 FastAPI 全局统一异常返回格式 :
把框架默认杂乱的报错,统一封装成你自定义的 ErrorResponse JSON 结构,前端固定一套格式解析错误,分两类捕获:
- 参数校验错误
RequestValidationError(422) - 手动抛出HTTP错误
HTTPException
一、两个Model:统一返回结构体
python
class ErrorDetail(BaseModel):
field: str # 出错字段名
message: str # 错误描述
code: str # 错误类型编码
class ErrorResponse(BaseModel):
error: str # 错误大类
message: str # 简要提示
details: Optional[List[ErrorDetail]] = None # 明细数组,没有则null
# default_factory:每次报错实例化时生成当前时间,不会类加载固定时间
timestamp: datetime = Field(default_factory=datetime.now)
details: Optional[List[ErrorDetail]] = None:可选字段- 参数校验失败 → 赋值
errors列表 - 普通HTTP异常 → 不传,自动=
None
- 参数校验失败 → 赋值
default_factory=datetime.now:工厂函数,每次报错动态生成当前时间 ,解决直接写datetime.now()只在类定义时生成一次的坑。
最终返回JSON样例(422参数错误)
json
{
"error": "Validation Error",
"message": "请求数据验证失败",
"details": [
{
"field": "username",
"message": "字段不能为空",
"code": "missing"
}
],
"timestamp": "2026-06-02Txx:xx:xx"
}
普通HTTP异常返回样例(如404/403)
json
{
"error": "HTTP 403",
"message": "没有权限访问",
"details": null,
"timestamp": "..."
}
二、第一个异常捕获:@RequestValidationError(入参校验422)
python
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = []
for error in exc.errors():
# loc是字段位置元组,如(body,username) → "body.username"
field='.'.join(str(x) for x in error['loc'])
errors.append(ErrorDetail(field=field,message=error['msg'],code=error['type']))
return JSONResponse(
status_code=422,
content=ErrorResponse(
error="Validation Error",
message="请求数据验证失败",
details=errors
).dict()
)
exc.errors():拿到所有校验失败明细,loc字段路径、msg错误信息、type错误编码;'.'.join(...):把("body","email")→body.email,前端一眼知道哪个字段错;- 组装
List[ErrorDetail]塞进details。
触发场景:入参类型错、缺少必填字段、正则不匹配、Field校验失败、@validator抛错。
三、第二个异常捕获:StarletteHTTPException(手动抛HTTP错误)
python
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
return JSONResponse(
status_code=exc.status_code,
content=ErrorResponse(
error=f"HTTP {exc.status_code}",
message=exc.detail
).dict()
)
- 触发场景:代码里
raise HTTPException(status_code=403, detail="无权操作") - 没有字段错误,所以不传details,自动为None。
四、关键知识点串联你前面学的内容
Optional[List[ErrorDetail]] = NoneOptional:允许字段为null;=None:字段可选,不赋值默认null;
default_factory=datetime.now工厂函数- 不在类定义时生成时间,每次实例ErrorResponse自动执行now()生成新时间,规避可变/固定值坑;
- BaseModel.dict():Pydantic模型转字典,作为接口返回JSON。
五、扩展:后续自定义业务异常
以后自己抛业务异常,再加一个@exception_handler即可继续复用ErrorResponse结构,全项目错误格式统一。
认识 @app.exception_handler(异常类)
@app.exception_handler(异常类):全局异常捕获注册器
作用:拦截项目中抛出的指定异常类型,不走 FastAPI 默认返回,改用你自定义函数构造 JSON 返回。
本质:把「异常类 ↔ 自定义处理函数」绑定。
1)@app.exception_handler(RequestValidationError)
作用
捕获 Pydantic/接口入参校验异常 → 422
前端传参错误、类型不对、缺少必填、Field(pattern)、@validator 校验失败都会抛出这个异常。
使用场景
- Query、Path、Body 请求参数校验不通过
- 入参格式错误:int 传字符串、邮箱正则不匹配、必填字段为空
- Pydantic 模型校验全部报错统一格式化(就是你上面把零散错误组装成 details 数组)
- 固定返回
422状态码
2)@app.exception_handler(StarletteHTTPException)
作用
捕获代码手动抛出的 raise HTTPException(),对应 400/403/404/500 等各类http错误码。
FastAPI 的
HTTPException继承自StarletteHTTPException,所以捕获父类就能接住所有raise HTTPException。
使用场景
业务代码手动抛错:
python
from fastapi import HTTPException
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if not permission:
raise HTTPException(status_code=403, detail="权限不足")
捕获后统一包装成 ErrorResponse 结构,details=null。
二、两者关键区别
| 对比 | RequestValidationError | StarletteHTTPException |
|---|---|---|
| 谁抛出 | FastAPI框架自动抛(参数校验失败) | 程序员手动 raise HTTPException |
| 状态码固定 | 固定 422 Unprocessable Entity |
自定义 status_code(403、404、400等) |
| 错误来源 | 请求入参不符合Model规则 | 业务逻辑主动判定异常 |
| 返回details | 有字段错误明细 details:[{}] |
无明细 details:null |
三、其他常用异常捕获(扩展,项目标配3大类)
1. 全局未知异常 Exception(兜底捕获500)
捕获代码BUG:空指针、索引越界、数据库报错、运算错误等所有未捕获异常。
python
@app.exception_handler(Exception)
async def global_err_handler(request: Request, exc: Exception):
return JSONResponse(
status_code=500,
content=ErrorResponse(
error="Server Error",
message="服务器内部异常"
).model_dump()
)
场景: 代码运行报错,统一返回规范JSON,不抛原生堆栈给前端。
2. 自定义业务异常(最常用,项目必加)
自己定义 BusinessException,业务主动抛:raise BusinessException(code=10001, msg="账号已被禁用")
python
# 1.自定义异常
class BusinessException(Exception):
def __init__(self, code: int, msg: str):
self.code = code
self.msg = msg
# 2.注册捕获
@app.exception_handler(BusinessException)
async def business_err_handler(request, exc: BusinessException):
return JSONResponse(
status_code=200,
content=ErrorResponse(
error=f"BIZ_{exc.code}",
message=exc.msg
).model_dump()
)
场景:业务自定义错误码,如10001账号冻结、10002余额不足,HTTP状态码仍然200,靠业务code区分错误。
3. RequestBodyValidationError、WebSocketException(小众)
- WebSocketException:websocket连接异常捕获
- JSONDecodeError:请求体不是合法JSON报错(传非json字符串)
四、异常捕获执行优先级(从上到下)
- 精准异常(RequestValidationError / HTTPException / 自定义业务异常)优先匹配
- 最后兜底
Exception捕获所有漏网异常
五、总结背诵版(面试可用)
RequestValidationError:框架自动抛、入参校验错、固定422、带字段详情;StarletteHTTPException:手动raise HTTPException、自定义http状态码、无字段详情;- 项目标配三类捕获:参数422、手动HTTP异常、全局500兜底、自定义业务异常。
RequestValidationError、StarletteHTTPException、BusinessException 区别
RequestValidationError、StarletteHTTPException是框架官方提前帮你写好的异常类;BusinessException是你的业务独有错误,FastAPI不知道你的业务规则,必须自己新建。
1. RequestValidationError(框架内置,拿来直接用)
FastAPI + Pydantic 提前内置定义好了这个类,安装完依赖代码就存在:
只要前端传参错了(少字段、格式错、正则不通过),框架内部代码自动抛出这个异常 ,你只需要导入、注册捕获就行,不用手写 class xxx(Exception)。
类比:
就像手机自带闹钟APP,拿来直接用,不用自己开发闹钟。
2. BusinessException(必须手动自定义)
FastAPI 只管通用HTTP错误、参数错误,它不懂你的业务:余额不足、账号封禁、手机号已注册、权限不够 。
这类业务报错是你项目独有的,框架不可能提前预知,所以:
- 自己新建异常类
BusinessException - 业务代码里手动
raise BusinessException(10001,"账号禁用") - 注册异常捕获,统一格式化返回
类比:
你需要一个记账APP,手机没有,只能自己开发。
通用规律:
✅ 框架层面的错误:参数错、路径错、http状态错误 → 内置异常,直接导入
✅ 业务层面的错误:产品逻辑报错 → 自己定义异常类
二、项目日常开发四类必用异常(条理划分,通俗说明+使用场景)
第一类:框架内置1:RequestValidationError → 422 参数校验异常
- 谁抛:FastAPI自动抛,不用手动raise
- 触发场景
前端接口传参不符合Pydantic模型:
必填字段没传、字符串传数字、邮箱格式错误、@validator校验失败 - 处理作用
把框架默认零散报错,统一包装成固定JSON,带上错误字段名、错误提示。
第二类:框架内置2:StarletteHTTPException(对应HTTPException)
- 谁抛:开发人员手动 raise HTTPException
- 触发场景:通用http错误
找不到数据404、没有访问权限403、请求非法400;
示例:raise HTTPException(status_code=404, detail="用户不存在") - 处理作用
统一返回项目标准格式,抛弃FastAPI默认返回样式。
第三类:自定义业务异常 BusinessException(项目最常用)
- 谁抛:手动raise,全业务逻辑使用
- 触发场景(业务逻辑错误,HTTP状态码仍返回200,靠自定义错误码区分)
- 用户登录:密码错误、账号冻结
- 订单:库存不足、订单已取消
- 数据库:手机号已被注册
示例:raise BusinessException(10002,"库存不足无法下单")
前后端约定:http=200,看返回体内部error编码判断业务失败。
第四类:内置父类 Exception(全局兜底500异常)
- 谁抛:程序运行意外报错,代码BUG
- 触发场景
数据库宕机、数组下标越界、空对象取值、第三方接口报错等未知异常 - 作用
捕获所有上面三类没接住的异常,统一返回"服务器异常",避免后端原生报错堆栈直接暴露给前端,安全规范。
三、四个异常捕获整体执行顺序(从精准→兜底)
- 参数错 → RequestValidationError(422)
- 手动HTTP错误 → StarletteHTTPException
- 业务自定义错误 → BusinessException
- 剩下所有未知代码BUG → Exception(500兜底)
四、精简总结(方便记忆)
- 框架通用报错 = 自带异常类,直接导入使用
- 项目独有业务报错 = 自己定义异常类
- 生产环境标准配置 = 4套异常捕获:422参数 + HTTP异常 + 自定义业务异常 + 全局500兜底
正则表达式
正则通俗讲解(结合你邮箱正则:r'^[\w\.-]+@[\w\.-]+\.\w+$')
一、正则是啥
一套字符匹配规则 ,用来批量判断字符串格式:邮箱、手机号、账号、密码格式校验,Pydantic 的 pattern 底层就是正则。
二、先拆解你这条邮箱正则
regex
^[\w\.-]+@[\w\.-]+\.\w+$
逐个符号含义:
| 符号 | 作用 |
|---|---|
^ |
开头锚点:字符串必须从此开始,不能前面多出字符 |
$ |
结尾锚点:字符串必须到此结束,不能后面多出字符 |
\w |
等价 [a-zA-Z0-9_]:字母、数字、下划线 |
[\w\.-] |
字符组:允许 \w、.、- 三种字符 |
+ |
前面内容出现1次或多次(不能空) |
@ |
固定字符,必须有一个@ |
\. |
普通小数点(.在正则默认任意字符,加反斜杠转义才是.) |
分段拆开邮箱:
用户名@域名.后缀
^[\w\.-]+→ 邮箱用户名部分@→ 固定@符号[\w\.-]+→ 域名(qq、163、company等)\.\w+$→.com/.cn/.org
user.abc-123@qq.com✅匹配
user@qq❌缺少后缀
#user@qq.com❌开头非法#
三、最常用正则元字符(日常开发够用)
1. 锚定首尾
^xxx:必须以xxx开头xxx$:必须以xxx结尾
2. 量词(控制出现几次)
*:0次或多次(可有可无)+:1次或多次(至少1个)?:0次或1次{n}:固定n次;{n,}至少n次;{n,m}n~m次
3. 字符集合 \[\]
[abc]:只能a/b/c其中一个[0-9]数字、[a-z]小写、[A-Z]大写[^abc]:除了abc以外任意字符(中括号里^代表取反)
4. 转义 \
. * + ? () [] 在正则有特殊含义,想要匹配本身符号 需要加 \
例:\. 就是普通小数点
5. 简写
\d=[0-9]数字\w=[a-zA-Z0-9_]\s空白(空格、制表符)
四、Python 使用正则两种场景
场景1:Pydantic pattern(你在用)
python
email:str=Field(...,pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')
框架内部自动正则校验,不满足抛422。
场景2:原生re模块手动校验
python
import re
pat = re.compile(r'^[\w\.-]+@[\w\.-]+\.\w+$')
res = pat.fullmatch("test@qq.com")
# 匹配成功返回对象,失败None
if res:
print("邮箱合法")
fullmatch:整串严格匹配(等价^$全包)
五、常用现成正则(项目直接抄)
- 手机号11位:
r'^1[3-9]\d{9}$' - 纯英文+数字用户名:
r'^[a-zA-Z0-9]+$'(你@validator里isalnum等效)
六、易错点
- 忘记
^$:\w+@qq.com会匹配aa@qq.comxxxx,前后多出字符也能过; .不转义:直接写.代表任意字符,想要小数点必须\.。