【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兜底、自定义业务异常。

RequestValidationError、StarletteHTTPException、BusinessException 区别

RequestValidationError、StarletteHTTPException 是框架官方提前帮你写好的异常类;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. . 不转义:直接写 . 代表任意字符,想要小数点必须 \.。
相关推荐
阳光九叶草LXGZXJ5 小时前
达梦数据库-报错-16-MERGE INTO提示:无效的列名[XX]
linux·运维·数据库·sql·学习
xxwl5855 小时前
RabbitMQ 学习笔记
笔记·学习·rabbitmq
李日华大战鸡红7 小时前
滑膜观测器(学习记录)
stm32·单片机·学习·matlab·机器人
传奇开心果编程8 小时前
【现代声明式UI学与练】第4课 列表渲染与 key——如何高效渲染列表、key 的作用、列表重排时的状态保持
学习·flutter·react native·ui·swiftui·android jetpack
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第5课 网络与数据层
android·网络·学习·ui·ios·kotlin·composer
yt004yt9 小时前
园区 VOC 绿岛集中治理项目,管网与监测系统设计要点
大数据·学习
一条破秋裤9 小时前
06_从设备读数据_字符设备read_write流程
学习
m4Rk_10 小时前
【论文阅读】Agent 记忆机制(92):EMPO²——让 Memory 从经验复用走向主动探索
论文阅读·人工智能·学习·开源·github
爱写代码的倒霉蛋10 小时前
FastAPI的接口实现流程讲解
fastapi
欣欣之王来了11 小时前
前端开发学习路线图:2026年Vue方向最新版
前端·学习·架构·项目·vue3教程