Search code examples
pythonfieldpydantic

automatically add fields_validators based on hint type with pydantic


I'd like to define once for all fields_validators function in a BaseModel class, and inherit this class in my model, and the validators should apply to the updated class attributes.

MWE

def to_int(v: Union[str, int]) -> int:
    if isinstance(v, str):
        if v.startswith("0x"):
            return int(v, 16)
        return int(v)
    return v


def to_bytes(v: Union[str, bytes, list[int]]) -> bytes:
    if isinstance(v, bytes):
        return v
    elif isinstance(v, str):
        if v.startswith("0x"):
            return bytes.fromhex(v[2:])
        return v.encode()
    else:
        return bytes(v)


class BaseModelCamelCase(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,
        alias_generator=AliasGenerator(
            validation_alias=lambda name: AliasChoices(to_camel(name), name)
        ),
    )

    # FIXME: should apply to int type from get_type_hints only
    @field_validator("*", mode="before")
    def to_int(cls, v: Union[str, int]) -> int:
        return to_int(v)

    # FIXME: should apply to bytes type from get_type_hints only
    @field_validator("*", mode="before")
    def to_bytes(cls, v: Union[str, bytes, list[int]]) -> bytes:
        return to_bytes(v)

class BaseTransactionModel(BaseModelCamelCase):
    nonce: int
    gas: int = Field(validation_alias=AliasChoices("gasLimit", "gas_limit", "gas"))
    to: Optional[bytes]
    value: int
    data: bytes
    r: int = 0
    s: int = 0

I tried to use model_validate but then I lost the alias parsing


Solution

  • To automatically add field validators based on type hints in Pydantic, you can use the Annotated type from typing_extensions along with Pydantic’s validator functions. This allows you to bind validation logic directly to a type, making your code more modular and reusable.

    Here’s an example of how you can achieve this:

    from typing import Any, List
    from typing_extensions import Annotated
    from pydantic import BaseModel, ValidationError
    from pydantic.functional_validators import AfterValidator
    
    # Define a validator function
    def check_positive(value: int) -> int:
        if value <= 0:
            raise ValueError(f'{value} is not a positive number')
        return value
    
    # Create an annotated type with the validator
    PositiveInt = Annotated[int, AfterValidator(check_positive)]
    
    # Use the annotated type in a Pydantic model
    class MyModel(BaseModel):
        positive_number: PositiveInt
    
    # Example usage
    try:
        model = MyModel(positive_number=10)
        print(model)
    except ValidationError as e:
        print(e)
    
    try:
        model = MyModel(positive_number=-5)
    except ValidationError as e:
        print(e)

    Explanation: Validator Function: check_positive ensures the value is a positive integer. Annotated Type: PositiveInt combines the int type with the check_positive validator using Annotated. Pydantic Model: MyModel uses PositiveInt for the positive_number field, automatically applying the validator. Benefits: Modularity: Validators are tied to types, making them reusable across different models. Clarity: The validation logic is separated from the model definition, improving readability.