【Fastapi学习笔记(4)】—— JsonScheme与数据验证、错误响应格式、正则表达式

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"
  1. 继承str、Enum:枚举值存字符串,前端只能传:admin/user/moderator 三者之一
  2. 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="年龄")
关键字细节
  1. Field(...)...代表必填字段,不能为空、必须传
    Field(None):配合Optional=非必填,前端可省略不传,默认None
  2. 内置校验参数:
    • min_length/max_length:字符串长度限制
    • regex:v1写法正则(v2推荐pattern),邮箱格式校验
    • ge=0、le=150:数字范围:age≥0、age≤150
  3. 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
        }
    }

作用:

  1. FastAPI自动生成接口文档(/docs)时,填入请求示例,前端调试接口一键填充样例JSON;
  2. Pydantic v2 改为 model_config={"json_schema_extra":{}},v1写法是Config内部schema_extra。

五、接口路由

python 复制代码
@app.post("/users", response_model=UserResponse)
async def create_user(user: UserCreate):
  1. user: UserCreate:FastAPI自动把POST的JSON请求体解析成UserCreate实例,自动全量校验;校验失败直接422,不会进入函数内部;
  2. response_model=UserResponse出参格式化,接口返回数据自动按照UserResponse模型过滤字段、格式化。

六、完整校验触发场景汇总(面试高频)

  1. username少于3位/超50位 → Field报错
  2. username带中文、下划线 → @validator报错
  3. email格式错误 → regex正则报错
  4. 密码小于8位 → Field报错;密码缺大写/小写/数字 → @validator报错
  5. age传负数、160 → ge/le数值报错
  6. role传admin123非法字符 → 枚举Enum报错
  7. full_name不传、age不传:合法,自动为None(Optional+默认None)

七、精简总结(背诵)

  1. 固定可选值用Enum枚举(角色);
  2. 长度、范围、正则等简单规则用Field
  3. 复杂多条件、自定义业务规则用@validator
  4. 非必填字段:Optional类型 = Field(None)
  5. Config.example用于接口文档展示请求样例;
  6. 入参注解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 结构,前端固定一套格式解析错误,分两类捕获:

  1. 参数校验错误 RequestValidationError(422)
  2. 手动抛出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()
    )
  1. exc.errors():拿到所有校验失败明细,loc 字段路径、msg错误信息、type错误编码;
  2. '.'.join(...):把 ("body","email")body.email,前端一眼知道哪个字段错;
  3. 组装 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

四、关键知识点串联你前面学的内容

  1. Optional[List[ErrorDetail]] = None
    • Optional:允许字段为null=None:字段可选,不赋值默认null;
  2. default_factory=datetime.now 工厂函数
    • 不在类定义时生成时间,每次实例ErrorResponse自动执行now()生成新时间,规避可变/固定值坑;
  3. 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字符串)

四、异常捕获执行优先级(从上到下)

  1. 精准异常(RequestValidationError / HTTPException / 自定义业务异常)优先匹配
  2. 最后兜底 Exception 捕获所有漏网异常

五、总结背诵版(面试可用)

  1. RequestValidationError框架自动抛、入参校验错、固定422、带字段详情
  2. StarletteHTTPException手动raise HTTPException、自定义http状态码、无字段详情
  3. 项目标配三类捕获:参数422、手动HTTP异常、全局500兜底、自定义业务异常。

RequestValidationErrorStarletteHTTPExceptionBusinessException 区别

RequestValidationErrorStarletteHTTPException 是框架官方提前帮你写好的异常类;BusinessException 是你的业务独有错误,FastAPI不知道你的业务规则,必须自己新建。

1. RequestValidationError(框架内置,拿来直接用)

FastAPI + Pydantic 提前内置定义好了这个类,安装完依赖代码就存在:

只要前端传参错了(少字段、格式错、正则不通过),框架内部代码自动抛出这个异常 ,你只需要导入、注册捕获就行,不用手写 class xxx(Exception)

类比:

就像手机自带闹钟APP,拿来直接用,不用自己开发闹钟。

2. BusinessException(必须手动自定义)

FastAPI 只管通用HTTP错误、参数错误,它不懂你的业务:余额不足、账号封禁、手机号已注册、权限不够

这类业务报错是你项目独有的,框架不可能提前预知,所以:

  1. 自己新建异常类 BusinessException
  2. 业务代码里手动 raise BusinessException(10001,"账号禁用")
  3. 注册异常捕获,统一格式化返回

类比:

你需要一个记账APP,手机没有,只能自己开发。

通用规律:

✅ 框架层面的错误:参数错、路径错、http状态错误 → 内置异常,直接导入

✅ 业务层面的错误:产品逻辑报错 → 自己定义异常类

二、项目日常开发四类必用异常(条理划分,通俗说明+使用场景)

第一类:框架内置1:RequestValidationError → 422 参数校验异常
  1. 谁抛:FastAPI自动抛,不用手动raise
  2. 触发场景
    前端接口传参不符合Pydantic模型:
    必填字段没传、字符串传数字、邮箱格式错误、@validator校验失败
  3. 处理作用
    把框架默认零散报错,统一包装成固定JSON,带上错误字段名、错误提示。

第二类:框架内置2:StarletteHTTPException(对应HTTPException)

  1. 谁抛:开发人员手动 raise HTTPException
  2. 触发场景:通用http错误
    找不到数据404、没有访问权限403、请求非法400;
    示例:raise HTTPException(status_code=404, detail="用户不存在")
  3. 处理作用
    统一返回项目标准格式,抛弃FastAPI默认返回样式。

第三类:自定义业务异常 BusinessException(项目最常用)

  1. 谁抛:手动raise,全业务逻辑使用
  2. 触发场景(业务逻辑错误,HTTP状态码仍返回200,靠自定义错误码区分)
  • 用户登录:密码错误、账号冻结
  • 订单:库存不足、订单已取消
  • 数据库:手机号已被注册
    示例:raise BusinessException(10002,"库存不足无法下单")

前后端约定:http=200,看返回体内部error编码判断业务失败。

第四类:内置父类 Exception(全局兜底500异常)

  1. 谁抛:程序运行意外报错,代码BUG
  2. 触发场景
    数据库宕机、数组下标越界、空对象取值、第三方接口报错等未知异常
  3. 作用
    捕获所有上面三类没接住的异常,统一返回"服务器异常",避免后端原生报错堆栈直接暴露给前端,安全规范

三、四个异常捕获整体执行顺序(从精准→兜底)

  1. 参数错 → RequestValidationError(422)
  2. 手动HTTP错误 → StarletteHTTPException
  3. 业务自定义错误 → BusinessException
  4. 剩下所有未知代码BUG → Exception(500兜底)

四、精简总结(方便记忆)

  1. 框架通用报错 = 自带异常类,直接导入使用
  2. 项目独有业务报错 = 自己定义异常类
  3. 生产环境标准配置 = 4套异常捕获:422参数 + HTTP异常 + 自定义业务异常 + 全局500兜底



正则表达式

正则通俗讲解(结合你邮箱正则:r'^[\w\.-]+@[\w\.-]+\.\w+$'

一、正则是啥

一套字符匹配规则 ,用来批量判断字符串格式:邮箱、手机号、账号、密码格式校验,Pydantic 的 pattern 底层就是正则。

二、先拆解你这条邮箱正则

regex 复制代码
^[\w\.-]+@[\w\.-]+\.\w+$

逐个符号含义:

符号 作用
^ 开头锚点:字符串必须从此开始,不能前面多出字符
$ 结尾锚点:字符串必须到此结束,不能后面多出字符
\w 等价 [a-zA-Z0-9_]:字母、数字、下划线
[\w\.-] 字符组:允许 \w、.- 三种字符
+ 前面内容出现1次或多次(不能空)
@ 固定字符,必须有一个@
\. 普通小数点(.在正则默认任意字符,加反斜杠转义才是.

分段拆开邮箱:

用户名@域名.后缀

  1. ^[\w\.-]+ → 邮箱用户名部分
  2. @ → 固定@符号
  3. [\w\.-]+ → 域名(qq、163、company等)
  4. \.\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:整串严格匹配(等价^$全包)

五、常用现成正则(项目直接抄)

  1. 手机号11位:r'^1[3-9]\d{9}$'
  2. 纯英文+数字用户名:r'^[a-zA-Z0-9]+$'(你@validator里isalnum等效)

六、易错点

  1. 忘记 ^$\w+@qq.com 会匹配 aa@qq.comxxxx,前后多出字符也能过;
  2. . 不转义:直接写 . 代表任意字符,想要小数点必须 \.
相关推荐
爱喝水的鱼丶1 小时前
SAP-ABAP:SAP 简单报表输出开发系列(共6篇) 第四篇:SAP 报表异常处理机制:数据校验与消息提示规范落地
开发语言·数据库·学习·算法·sap·abap
東雪木2 小时前
泛型、反射、注解(Spring 框架核心底层)专属复习笔记
java·windows·笔记·学习·spring
小陈phd3 小时前
多模态大模型学习笔记(四十七)——跨模态融合策略:早融合、中融合与晚融合核心解析
笔记·学习
进击的小头3 小时前
第7篇:MOS 管最全入门:原理、关键参数、选型、驱动与典型应用
经验分享·科技·嵌入式硬件·学习
叶子野格3 小时前
《C语言学习:文件操作》16
c语言·开发语言·c++·学习·visual studio
ZC跨境爬虫3 小时前
SQL学习日志 Day_3 :(SELECT查询语句入门)
数据库·sql·学习·oracle
ss2733 小时前
ai编程Trae cn生成图书管理系统(1)
java·数据库·spring boot·python·flask·fastapi
小郑加油4 小时前
一周读懂博弈论:从理性决策到信息博弈_Day2博弈论基础与战略思维
学习·管理学·经济学
A_humble_scholar4 小时前
C++11 学习笔记:统一初始化、右值引用与完美转发
c++·笔记·学习