fastapi: 参数校验时自定义报错信息(get/post/json/基于basemodel类/基于Annotated参数)

一,代码:

异常处理

    # 2. 捕获 FastAPI 参数校验异常 (422 Unprocessable Entity)
    @app.exception_handler(RequestValidationError)
    async def validation_exception_handler(request: Request, exc: RequestValidationError):
        # 获取当前请求路由的匹配结果
        route = request.scope.get("route")
        custom_errors = []
        str_errors = ''
        # 获取异常自带的错误详情
        errors = exc.errors()

        for error in errors:
            # loc 包含了出错字段的路径,例如 ("body", "username") 或 ("query", "age")
            print("error:", error )
            loc = error.get("loc", [])
            print("loc:", loc)
            field_name = loc[-1] if loc else None
            location_type = loc[0] if len(loc) >= 1 else None
            print("field_name:", field_name)
            description = "未定义描述"
            print("location_type:", location_type)
            # 如果是请求体(Body)校验失败,尝试从路由函数签名中找 Pydantic Model 并提取 description

            is_base = is_basemodel(route, field_name)   # 判断字段是否是基于BaseModel的子类
            print("is_base:", is_base)

            if route and hasattr(route, "dependant") and field_name:
                print("开始查找数据:", field_name)
                # 遍历路由中所有的 Query 参数定义
                all_flat_params = (
                        route.dependant.query_params +
                        route.dependant.path_params +
                        route.dependant.header_params +
                        route.dependant.cookie_params
                        # route.dependant.body_params
                )
                for param in all_flat_params:
                    print("dir_param:", dir(param))
                    # 匹配当前报错的字段名(比如 "q")
                    print("param.name:", param.name)
                    if param.name == field_name:
                        # 关键点:FastAPI 把 Query(description="...") 存放在了 param.field_info 中
                        if hasattr(param, "field_info") and getattr(param.field_info, "description", None):
                            description = param.field_info.description
                            print("description1:",description)
                            error_msg = f"{description}"
                            if str_errors == '':
                                str_errors = error_msg
                            else:
                                str_errors += ";" + error_msg
                            break

                body_flat_params = (
                        route.dependant.body_params
                )
                for param in body_flat_params:
                    print("dir_param:", dir(param))
                    # 匹配当前报错的字段名(比如 "q")
                    print("param.name:", param.name)
                    # if param.name == field_name:
                        # 关键点:FastAPI 把 Query(description="...") 存放在了 param.field_info 中
                    if hasattr(param, "field_info") and getattr(param.field_info, "description", None):
                        description = param.field_info.description
                        print("description1:",description)
                        error_msg = f"{description}"
                        if str_errors == '':
                            str_errors = error_msg
                        else:
                            str_errors += ";" + error_msg
                        break

            if (location_type == "body" or location_type == "query") and route and field_name:
                endpoint = route.endpoint
                # 遍历路由函数的参数,寻找继承自 BaseModel 的参数
                import inspect

                sig = inspect.signature(endpoint)
                print(sig.parameters.items())
                field_info = ''
                for param_name, param in sig.parameters.items():
                    model_cls = param.annotation
                    print("model_cls:",model_cls)
                    print(dir(model_cls))
                    if hasattr(model_cls, 'model_fields'):
                        field_info = model_cls.model_fields.get(field_name)
                        print("field_info:", field_info)
                        if field_info and field_info.description:
                            error_msg = f"{field_info.description}"
                            print("error_msg:", error_msg)
                            if str_errors == '':
                                str_errors = error_msg
                            else:
                                str_errors += ";" + error_msg
                            break
                    else:
                        print("没有model_fields属性")

            if str_errors == '':
                one_str_error = field_name + ":" + error['msg']
                str_errors = one_str_error

        content = BaseResponse(code=422, msg=str_errors, data=custom_errors).model_dump()
        return JSONResponse(
            status_code=422,
            content=content
        )

调用:使用Annotated参数

@router.post("/onepost")
def get_one_post(
        # 校验 Query (GET) 参数:长度必须在 3 到 10 之间
        # q: Annotated[str, Query(min_length=3, max_length=10)],
        # 校验 Body (POST) 参数:必须大于 0
        # product_id: Annotated[float, Body(gt=0.0)],
        product_id: Annotated[int, Body(gt=0,description="商品id,需要是大于0的整数")],
):
    print("测试参数检测:",)
    return {"id": product_id, "name": f"未知"}

@router.get("/oneprod")
def get_one_product(
        # 校验 Query (GET) 参数:长度必须在 3 到 10 之间
        # q: Annotated[str, Query(min_length=3, max_length=10)],
        # 校验 Body (POST) 参数:必须大于 0
        # product_id: Annotated[float, Body(gt=0.0)],
        product_id: Annotated[int, Query(gt=0,description="商品id,需要是大于0的整数")],
):
    print("测试参数检测:",)
    return {"id": product_id, "name": f"未知"}

基于basemodel类:get/post

# 1. 定义请求体数据模型
class ProductPage(BaseModel):
    category: str = Field(..., min_length=3, max_length=20,description="分类名长度是3-20个字符")
    page: int = Field(..., gt=0, description="页数需要大于0")
    per_page: int = Field(..., gt=0, description="每页数量需要大于0")


@router.get("/all")
async def get_all_products(prod_data: Annotated[ProductPage, Query()],
                           db: AsyncSession = Depends(get_db)):

    page = prod_data.page
    per_page = prod_data.per_page
    # 异步写法不能直接用 db.query(),必须用 select
    skip = (page-1) * per_page
    limit = per_page
    
    stmt = select(Product).offset(skip).limit(limit)
    result = await db.execute(stmt)  # 必须 await 数据库 I/O
    prods = result.scalars().all()

    # 异步查询总条数 (注意 await)
    count_stmt = select(func.count()).select_from(Product)
    total_items = await db.scalar(count_stmt)

    # 计算分页元数据
    total_pages = math.ceil(total_items / per_page) if total_items > 0 else 1
    has_prev = page > 1
    has_next = page < total_pages

    res = {
        "list": prods,
        "meta": {
            "total_items": total_items,
            "total_pages": total_pages,
            "current_page": page,
            "per_page": per_page,
            "has_prev": has_prev,
            "has_next": has_next,
            "prev_page": page - 1 if has_prev else None,
            "next_page": page + 1 if has_next else None
        }
    }

    return success(data=res)

# 1. 定义请求体数据模型
class ProductCreate(BaseModel):
    name: str = Field(..., min_length=3, max_length=20,description="商品名称,长度是3-20个字符")
    price: int = Field(..., gt=0, description="价格需要大于0")
    stock: int = Field(..., ge=0, description="库存不能小于0")

# 如果请求方式是get,使用: params: Annotated[FilterParams, Query()]

@router.post("/add")
async def create_user(
        prod_data: Annotated[ProductCreate, Form()],
        db: AsyncSession = Depends(get_db)):
    # 检查用户名是否已存在
    result = await db.execute(select(Product).where(Product.name == prod_data.name))
    existing_prod = result.scalar_one_or_none()
    if existing_prod:
        raise HTTPException(status_code=400, detail="产品名已被使用")

    # 创建新产品
    new_prod = Product(
        name=prod_data.name,
        stock=prod_data.stock,
        price = prod_data.price,
    )

    # 添加到数据库
    db.add(new_prod)
    await db.commit()  # 提交事务
    await db.refresh(new_prod)  # 刷新,获取数据库生成的字段(如id)

    return success(data=new_prod)

基于basemodel类:json

# 1. 定义请求体数据模型
class UserLogin(BaseModel):
    username: str = Field(..., min_length=3, max_length=20,description="用户名,长度是3-20个字符")
    age: int = Field(..., ge=18, le=100, description="年龄必须在18到100岁之间")
    password: str = Field(..., min_length=6, description="密码长度最少6个字符")

@router.post("/login")
async def account_login(user: UserLogin):
    user_info = {"age": user.age, "name": f"用户{user.username}"}
    return success(data=user_info)

 

二,测试效果:

get

image

get

image

post

image

post:

image

post:

image

 

posted @ 2026-06-21 13:09  刘宏缔的架构森林  阅读(12)  评论(0)    收藏  举报