How to go through all Pydantic validators even if one fails, and then raise multiple ValueErrors in a FastAPI response?
Question:
Is it possible to call all validators to get back a full list of errors?
@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
The current behavior seems to call one validator at a time.
My Pydantic class:
class User(BaseModel):
email: EmailStr
password: SecretStr
If I did not include the email
, or password
, field on a request then I would get both validation failures in an array, which is what I want to do for the password
field, but the current behavior seems to call one, and if it fails then throws the error immediately.
Answers:
You cannot use Pydantic’s validators like that; it always looks one of them.
To achieve your answer, you can use following 2 methods
1 – You can use one main validator which checks all conditions
@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 – You can use duplicate variable in model to check condition
class User(BaseModel):
email: EmailStr
password: SecretStr
password2: SecretStr
and obviously, your decorators should be:
@validator('password', always=True)
def validate_password1(cls, value):
and
@validator('password2', always=True)
def validate_password2(cls, value):
UPDATE: The OP wants to raise all errors, so the updated answer as follows.
In addition to 1st bullet, you may try something like that:
@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.')
However, be careful when returning the value
. You may try to add one more custom exception in your main code.
You can’t raise multiple Validation errors/exceptions for a specific field, in the way you demonstrate in your question. Suggested solutions are given below.
Option 1
Concatenate error messages using a single variable, and raise the ValueError
once at the end (if errors occured):
@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
In the case that all the above conditional statements are met, the output will be:
{
"detail": [
{
"loc": [
"body",
"password"
],
"msg": "Password must be at least 8 characters long. Password should contain at least one lowercase character.",
"type": "value_error"
}
]
}
Option 2
Raise 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
Since FastAPI seems to be adding the loc
attribute itself, loc
would end up having the field
name (i.e., password
) twice, if it was added in the ErrorWrapper
, using the loc
attribute (which is a required parameter). Hence, you could leave it empty (using None
), which you can later remove through a validation exception handler, as shown below:
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(content=jsonable_encoder({"detail": exc.errors()}), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
In the case that all the above conditional statements are met, the output will be:
{
"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"
}
]
}
Is it possible to call all validators to get back a full list of errors?
@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
The current behavior seems to call one validator at a time.
My Pydantic class:
class User(BaseModel):
email: EmailStr
password: SecretStr
If I did not include the email
, or password
, field on a request then I would get both validation failures in an array, which is what I want to do for the password
field, but the current behavior seems to call one, and if it fails then throws the error immediately.
You cannot use Pydantic’s validators like that; it always looks one of them.
To achieve your answer, you can use following 2 methods
1 – You can use one main validator which checks all conditions
@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 – You can use duplicate variable in model to check condition
class User(BaseModel):
email: EmailStr
password: SecretStr
password2: SecretStr
and obviously, your decorators should be:
@validator('password', always=True)
def validate_password1(cls, value):
and
@validator('password2', always=True)
def validate_password2(cls, value):
UPDATE: The OP wants to raise all errors, so the updated answer as follows.
In addition to 1st bullet, you may try something like that:
@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.')
However, be careful when returning the value
. You may try to add one more custom exception in your main code.
You can’t raise multiple Validation errors/exceptions for a specific field, in the way you demonstrate in your question. Suggested solutions are given below.
Option 1
Concatenate error messages using a single variable, and raise the ValueError
once at the end (if errors occured):
@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
In the case that all the above conditional statements are met, the output will be:
{
"detail": [
{
"loc": [
"body",
"password"
],
"msg": "Password must be at least 8 characters long. Password should contain at least one lowercase character.",
"type": "value_error"
}
]
}
Option 2
Raise 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
Since FastAPI seems to be adding the loc
attribute itself, loc
would end up having the field
name (i.e., password
) twice, if it was added in the ErrorWrapper
, using the loc
attribute (which is a required parameter). Hence, you could leave it empty (using None
), which you can later remove through a validation exception handler, as shown below:
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(content=jsonable_encoder({"detail": exc.errors()}), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
In the case that all the above conditional statements are met, the output will be:
{
"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"
}
]
}