I am trying to set up custom validation for pydantic models. According to the the pydantic docs, custom validators can be applied to a field using the Annotated Pattern or field_validator() decorator applied to a class method:
from typing import Annotated
from pydantic import AfterValidator, BaseModel, ValidationError
def is_even(value: int) -> int:
if value % 2 == 1:
raise ValueError(f'{value} is not an even number')
return value
class Model(BaseModel):
number: Annotated[int, AfterValidator(is_even)]
try:
Model(number=1)
except ValidationError as err:
print(err)
"""
1 validation error for Model
number
Value error, 1 is not an even number [type=value_error, input_value=1, input_type=int]
"""
field_validator() decorator:
from pydantic import BaseModel, ValidationError, field_validator
class Model(BaseModel):
number: int
@field_validator('number', mode='after')
@classmethod
def is_even(cls, value: int) -> int:
if value % 2 == 1:
raise ValueError(f'{value} is not an even number')
return value
try:
Model(number=1)
except ValidationError as err:
print(err)
"""
1 validation error for Model
number
Value error, 1 is not an even number [type=value_error, input_value=1, input_type=int]
"""
I have tried both the Annotated and field_validator decorator methods, but each one causes an error. The error occurs when FastAPI perform the initial validation on the pydantic model received as a request from a client:
TypeError: Object of type ValueError is not JSON serializable
Upon further inspect from the RequestValidationError, the error handler displayed the issue that the raised ValueError is not converted to the correct dict for the ctx key, causing the JSON Serializable error since the ValueError is not serializable:
fastapi.exceptions.RequestValidationError: [{'type': 'value_error', 'loc': ('body', 'email'), 'msg': 'value is not a valid email address: An email address must have an @-sign.', 'input': 'user+dmiexample.com', 'ctx': {'reason': 'An email address must have an @-sign.'}}, {'type': 'value_error', 'loc': ('body', 'password'), 'msg': 'Value error, password must have one uppercase character', 'input': 'password', 'ctx': {'error': ValueError('password must have one uppercase character')}}]
I haven't really figured out a way to convert the ValueError to the expected {'reason': 'validation_msg'} because according to the official validation pydantic docs, they use ValueError for their validation functions. Meaning, the solution would deviate from the official pydantic documentation and the fastAPI documentation only mentions using Field for basic validation.
Both the EmailStr and constr datatype to a pydantic field as documented here, have produced the correct output in terms of validating it correctly:
from pydantic import BaseModel, EmailStr, constr, ValidationError
# Defome User model
class User(BaseModel):
email: EmailStr
password: constr(
min_length=8,
max_length=20
)
I just can't get the ValueError to be converted correctly.
requirements.txt:
fastapi==0.116.1
fastapi-cli==0.0.8
fastapi-cloud-cli==0.1.5
pydantic==2.11.7
pydantic_core==2.33.2
sqlmodel==0.0.24
Model:
class UserCreate(SQLModel):
email: EmailStr
password: Annotated[str, AfterValidator(ApiValidators.is_password_valid)]
Validator:
class ApiValidators:
@staticmethod
def is_password_valid(value: str) -> str:
"""Checks if a password is valid and strong"""
if len(value) < 8:
raise ValueError(f"{value} must have at least 8 characters")
if not re.search(ApiPatterns.LOWER_CASE_CHARS, value):
raise ValueError(f"{value} must have one lowercase character")
if not re.search(ApiPatterns.UPPER_CASE_CHARS, value):
raise ValueError(f"{value} must have one uppercase character")
if not re.search(ApiPatterns.DIGITS, value):
raise ValueError(f"{value} must have atleast one digit")
if not re.search(ApiPatterns.SPECIAL_CHARS, value):
raise ValueError(f"{value} must have atleast one special character")
return value