即使一个失败,如何通过所有 Pydantic 验证器,然后在 FastAPI 响应中引发多个 ValueErrors?

How to go through all Pydantic validators even if one fails, and then raise multiple ValueErrors in a FastAPI response?

是否可以调用所有验证器以返回完整的错误列表?

@validator('password', always=True)
def validate_password1(cls, value):
    password = value.get_secret_value()

    min_length = 8
    if len(password) < min_length:
        raise ValueError('Password must be at least 8 characters long.')

    return value

@validator('password', always=True)
def validate_password2(cls, value):
    password = value.get_secret_value()

    if not any(character.islower() for character in password):
        raise ValueError('Password should contain at least one lowercase character.')

    return value

当前行为似乎一次调用一个验证器。

我的 Pydantic class:

class User(BaseModel):
    email: EmailStr
    password: SecretStr

如果我没有在请求中包含 emailpassword 字段,那么我会在数组中得到两个验证失败,这就是我想为 password 字段,但当前行为似乎调用一个,如果失败则立即抛出错误。

你不能那样使用 Pydantic's validators;它看起来总是其中之一。

为了得到你的答案,你可以使用以下两种方法

1 - 您可以使用一个检查所有条件的主验证器

@validator('password', always=True)
def validate_password(cls, value):
    password = value.get_secret_value()

    validate_password1(password)
    validate_password2(password)

    return value
    
def validate_password1(password):

    min_length = 8
    if len(password) < min_length:
        raise ValueError('Password must be at least 8 characters long.')


def validate_password2(password):

    if not any(character.islower() for character in password):
        raise ValueError('Password should contain at least one lowercase character.')


2 - 您可以在模型中使用重复变量来检查条件

class User(BaseModel):
    email: EmailStr
    password: SecretStr
    password2: SecretStr

显然,您的装饰器应该是:

@validator('password', always=True)
def validate_password1(cls, value):

@validator('password2', always=True)
def validate_password2(cls, value):

更新: OP 想提出所有错误,所以更新后的答案如下。

除了第一个项目符号,您还可以尝试类似的方法:

@validator('password', always=True)
def validate_password(cls, value):
    password = value.get_secret_value()

    try:
        validate_password1(password)
    except Exception as e:
        print('First error: ' + str(e))
        
    try:
        validate_password2(password)
    except Exception as e:
        print('Second error: ' + str(e))

    return value
    
def validate_password1(password):

    min_length = 8
    if len(password) < min_length:
        raise ValueError('Password must be at least 8 characters long.')


def validate_password2(password):

    if not any(character.islower() for character in password):
        raise ValueError('Password should contain at least one lowercase character.')


但是,返回 value 时要小心。您可以尝试在主代码中再添加一个自定义异常。

您不能以您在问题中展示的方式为特定领域提出多个 Validation errors/exceptions。下面给出了建议的解决方案。

选项 1

使用单个变量连接错误消息,并在末尾引发一次 ValueError(如果发生错误):

@validator('password', always=True)
def validate_password1(cls, value):
    password = value.get_secret_value()
    min_length = 8
    errors = ''
    if len(password) < min_length:
        errors += 'Password must be at least 8 characters long. '
    if not any(character.islower() for character in password):
        errors += 'Password should contain at least one lowercase character.'
    if errors:
        raise ValueError(errors)
        
    return value

在满足上述所有条件语句的情况下,输出将是:

{
  "detail": [
    {
      "loc": [
        "body",
        "password"
      ],
      "msg": "Password must be at least 8 characters long. Password should contain at least one lowercase character.",
      "type": "value_error"
    }
  ]
}

选项 2

提高 ValidationError directly, using a list of ErrorWrapper class.

from pydantic import ValidationError
from pydantic.error_wrappers import ErrorWrapper

@validator('password', always=True)
def validate_password1(cls, value):
    password = value.get_secret_value()
    min_length = 8
    errors = []
    if len(password) < min_length:
        errors.append(ErrorWrapper(ValueError('Password must be at least 8 characters long.'), loc=None))
    if not any(character.islower() for character in password):
        errors.append(ErrorWrapper(ValueError('Password should contain at least one lowercase character.'), loc=None))
    if errors:
        raise ValidationError(errors, model=User)
        
    return value

由于 FastAPI 似乎正在添加 loc 属性本身,因此 loc 最终将具有 field 名称(即 password)两次,如果它是使用 loc 属性(这是必需参数)添加到 ErrorWrapper 中。因此,您可以将其留空(使用 None),稍后您可以通过 validation exception handler 将其删除,如下所示:

from fastapi import Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    for error in exc.errors(): 
        error['loc'] = [x for x in error['loc'] if x]  # remove null attributes
        
    return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content=jsonable_encoder({"detail": exc.errors()}))

在满足上述所有条件语句的情况下,输出将是:

{
  "detail": [
    {
      "loc": [
        "body",
        "password"
      ],
      "msg": "Password must be at least 8 characters long.",
      "type": "value_error"
    },
    {
      "loc": [
        "body",
        "password"
      ],
      "msg": "Password should contain at least one lowercase character.",
      "type": "value_error"
    }
  ]
}