ITPub博客

首页 > 数据库 > 数据库开发技术 > FastApi(自用脚手架)+Snowy搭建后台管理系统(9)-自定义pydantic参数校验错误时的中文错误提示

FastApi(自用脚手架)+Snowy搭建后台管理系统(9)-自定义pydantic参数校验错误时的中文错误提示

原创 数据库开发技术 作者:程序员小钟同学 时间:2023-02-07 00:00:00 3 删除 编辑

前言

长久以来比较困扰的一个关于【pydantic的问题参数校验提示的问题】,在突发奇想的情况,有一个迂回的实现方案,它可以实现的效果就是类似我们很久以前渴望实现的类似wtform类似自定义的【中文】提示。

wtform自定义错误提示

以下示例是wtform的十点类型错误时候定义的错误信息提示。

from core.forms import BaseForm,validate_form,validate_back_form
from wtforms import DateTimeField, PasswordField, FieldList, IntegerField, StringField
from wtforms.validators import DataRequired, Regexp, EqualTo, length, Optional, NumberRange

class LoginForm(BaseForm):
    username = StringField(validators=[DataRequired(message='username 必须传入')])
    password = StringField(validators=[DataRequired(message='password 必须传入')])

class SysPermissionForm(BaseForm):
    token = StringField(validators=[DataRequired(message='token 必须传入')])

而当我们的FastApi框架中涉及到定义错误信息提示时候,则没对应的字段信息进行定义,如下一些定义:

class AuthAccountPasswordLoginParam(SchemaBase):
    '''
    账号密码登录参数
    '
''
    account: str = Field(..., title="账号", description='手机号码不能为空')
    password: str = Field(..., title="密码", description='手机号码不能为空')
    validCode: str = Field(..., title="验证码", description='验证码不能为空')
    validCodeReqNo: str = Field(None, title="验证码请求号", description='')
    device: str = Field(None, title="设备信息", description='')

或者是:


class UserModel(SchemaBase):
    name: str = Field(..., title="手机号")
    age: int = None
    sex: str = 'male'
    address: str = None
    mobile: str = None

当FastApi出现错误的时候,我们则无法定义上述类似的错误提示文本信息。对于有强迫感的人来说,还是有点不爽。

pydantic自定义中文错误模板

1. 问题表现

对于FastApi框架不管是GET还是POST中的参数,我个人的习惯还是喜欢定义为一个模型来传入,如下代码所示:

定义参数模型:

class SchemaBase(BaseSchema):
    pass
class UserModel(SchemaBase):
    name: str = Field(..., title="手机号")
    age: int = None
    sex: str = 'male'
    address: str = None
    mobile: str = None

对于如果是查询参数来说,比较棘手的一个问题是UserModel需要做一层依赖转换它还会转为查询参数,如下接口定义所示:

@RestParameter(modelss=UserModel)
@get("/b/doLogin2ceshi/", summary='执行登入接口')
async def doLoginceshi(self,suemolde:UserModel =Depends(UserModel)):
    # TypeError: Failed to execute 'fetch' on 'Window': Request with GET/HEAD method cannot have body.
    return "AAAAAAAAAAAA"


@RestParameter(modelss=UserModel)
@post("/b/doLogin2ceshi2222/", summary='执行登入接口')
async def doLoginceshi22222(self,suemolde:UserModel):
    # TypeError: Failed to execute 'fetch' on 'Window': Request with GET/HEAD method cannot have body.
    return "AAAAAAAAAAAA"

上面代码中我们注意到在get中,我们对suemolde:UserModel =Depends(UserModel))做了一层转换,这样才可以实现模型转化为查询参数转化的处理。

基于上面的模型定义中,如果我们的不管是在get还是在post中如果没有传入“name”参数的情况下,都会返回对应的错误提示信息,如下是我们的全局拦截错误处理图示:

而当我们的没有传入“name”参数则会抛出如下图所示错误提示,:

文本提示如下所示:

{
 "message": "参数校验错误,请检查提交的参数信息",
 "code": 10031,
 "success": false,
 "result": {
  "message": "Validation error.",
  "fields": [{
   "name": "asas",
   "message": "Value is not a valid integer."
  }],
  "errors": [{
   "location": "query -> asas",
   "message": "value is not a valid integer",
   "error_type": "type_error.integer"
  }]
 },
 "timestamp": 1672809002591
}

总体来说,我们对此的错误提示信息还是有点不爽,因为不够干净。更多的时候我们是喜欢对特定的字段,特定的错误,抛出特定的错误模板文本信息,如下图所示:

2.关键解决思路

其实解决问题思路一开始我是打算通过反射获取对应的抛出异常的时候对应的模型的对象进行映射,我们的错误模本的定制,如下图所示:

但是对应的exc: Union[RequestValidationError, ValidationError]只会在POST的情况下携带我们的当前请求的对应的模型对象,对于GET转为为查询参数后的模型则无从知晓,所以我们需要想方设法给我们的当前请求处理异常的透传一个当前对应的查询的参数模型对象。然后通过对应的请求上下文获取当前对应的模型对象。所以我们需要定义个装饰器,用于给当前路由透传一个模型,我用到思路就是结合依赖注入和请求上下文设置一个当前模型参数。如下代码所示:

def RestParameter(modelss:Type[MODEL_SCHEMA]):
    def call_func(fun: AnyCallable) -> Callable[[AnyCallable], AnyCallable]:
        if hasattr(fun, '_route_args'):
            route_args: ApiRouteArgs = getattr(fun, '_route_args')
            def depends_set_model_(request: Request):
                request.state.parameter_model = modelss
                return request
            parameter: Optional[params.Depends] = Depends(depends_set_model_)
            route_args.dependencies = list(route_args.dependencies).append(parameter) if route_args.dependencies else [ Depends(depends_set_model_)]
        return fun
    return call_func

上面的装饰器依赖于我们的当前一个路由参数参数对象的封装,ApiRouteArgs,它本质就是对我们的要添加的端点函数对象(路由函数)添加对应的参数,其中包括一个依赖注入项的"dependencies"列表。通过在路由参数(路由路径参数中的依赖项)依赖项实现“parameter_model”参数的透传。

定义模型错误模板信息

完成透传之后,我们可以根据错误拦截的时候传递的requets来获取到当前注入的模型对象,而这个模型对象是我们的实现定制的关键,如下模型类代码所示:

MODEL_SCHEMA = TypeVar("MODEL_SCHEMA", bound=SchemaBase)
class UserModel(SchemaBase):
    name: str = Field(..., title="姓名",min_length=6)
    age: int = None
    sex: str = 'male'
    address: str = None
    mobile: str = None

    class Config:
        error_msg = {
            # error_type根据定义的错误类型抛出之定义的错误信息
            'name': {
                'value_error.missing': '姓名不能为空',
                'value_error.any_str.min_length': '字符串长度至少6位以上'
            }
        }

首先我们在Config中自定义实现了一个error_msg定制的错误模板,其中对应的“name”对于我们可能需要实现的校验错误定制了错误提示信息,比如:

  • 必传验证错误的时候(value_error.missing),则提示姓名不能为空
  • 如果参数长度最小不够的时候(value_error.any_str.min_length),则提示字符串长度至少6位以上

我们需要做就是在抛出错误的时候需要获取关键信息有:

  • 错误的字段信息
  • 错误字段类型

最终我们的错误的拦截处理如下图示:

如此我们的就完成了对应的自定义RequestValidationError参数校验中文错误提示,更多的错误类型则,我们需要针对性进行定义了!!最终效果图示如下:

优化错误模板定义

上面的模型的定义的时候对于部分提示信息其实是整体可以共享,所以我们进行一定抽取即可,如下完成后错误模板信息和模型定义如下所示:

_any_str_error = {
    'value_error.missing': '{name} 不能为空',
    'value_error.any_str.min_length': '{name} 字段字符串长度至少6位以上'
}
_any_int_error = {
    'value_error.number.not_gt': '{name} 参数字段信息必须大于10',
    'value_error.number.not_le': '{name} 参数字段信息小于150',
    "type_error.integer": "{name}参数必须是一个int类型的参数"
}
_any_int_float = {
    'value_error.number.not_gt': '{name} 参数字段信息必须大于10',
    'value_error.number.not_le': '{name} 参数字段信息小于150',
    "type_error.float": "{name} 参数必须是一个float类型的参数"
}
class UserModel(SchemaBase):
    name: str = Field(..., title="姓名", min_length=6)
    age: int = Field(..., title="年龄", gt=10, le=150)
    sex: str = 'male'
    address: str = None
    mobile: str = None
    class Config:
        error_msg = {
            # error_type根据定义的错误类型抛出之定义的错误信息
            'name': _any_str_error,
            'age': _any_int_error,
        }

上面中,我针对对应的字段错相关占位处理,直接提取错误的字段信息即可。然后对于一些比较特殊的类型,如:

value_error.jsondecode 这种类型的错误会导致我们的依赖注入项注入我们的   request.state.parameter_model = modelss 是失效,也就是无法注入parameter_model对象成功。

而这种异常错误引发原因如下图所示,主要在POST中我们的字段转换异常的问题:

注意上图中我们的age: 本来应该引发的是一个type_error.integer异常,但是目前为止的情况出现确是上面的value_error.jsondecode 异常。按之前探查源码的情况,初步判断是因为我们的模型在进行转换序列化的时候,直接影响到了其他依赖项执行,(但是按理,应该存在先优先执行的,后面模型序列化检测才是后置,有点怪异),所以目前只能做兼容处理,最终处理代码如下:

        @app.exception_handler(RequestValidationError)
        async def validation_exception_handler(_request: Request, exc: Union[RequestValidationError, ValidationError]):
            status_code = getattr(exc, "status_code", 400)
            curr_model = validation_error(_request,exc)
            if curr_model:
                return SnowyParameterException(http_status_code=status_code,
                                               msg=APIValidationError.from_pydantic_onlymessage(exc),
                                               data=None)
            else:
                for err in exc.errors():
                    if err["type"] == 'value_error.jsondecode':
                        err["msg"] = '部分字段类型转换错误,请核对!'
                        return SnowyParameterException(http_status_code=status_code,
                                                       msg=APIValidationError.from_pydantic_onlymessage(exc),
                                                       data=None)

                return SnowyParameterException(http_status_code=status_code,
                                               data=APIValidationError.from_pydantic(exc).dict(exclude_none=True))

最终处理后关于上述错误提示如下图所示:

其他错误类型提示如图所示:

至此,关于【自定义RequestValidationError参数校验中文错误提示的】相关介绍分享已完成!以上内容分享纯属个人经验,仅供参考!

文笔有限,如有笔误或错误!欢迎批评指正!感谢各位大佬!有什么问题也可以随时交流!

结尾

END

简书:https://www.jianshu.com/u/d6960089b087

掘金:https://juejin.cn/user/2963939079225608

公众号:微信搜【程序员小钟同学】

新开QQ群号,欢迎随时加群交流,相互学习。QQ群号:247491107

小钟同学 | 文 【欢迎一起学习交流】| QQ:308711822


原文作者:程序员小钟同学;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2021-02-20

  • 博文量
    224
  • 访问量
    111682