pythonvalidationpydanticcode-duplication

Pydantic - apply validator on all fields of specific type


In my project, all pydantic models inherit from a custom "base model" called GeneralModel.

This enables to configure the same behavior for the entire project in one place.

Let's assume the following implementation:

from pydantic import BaseModel


class GeneralModel(BaseModel):
    class Config:
        use_enum_values = True
        exclude_none = True

One particularly desired behavior is to perform a validation on all fields of a specific type.

Any ideas how to achieve this using my GeneralModel? Different approaches are blessed as well.


Solution

  • Quote from the Pydantic validators documentation:

    a single validator can also be called on all fields by passing the special value '*'

    and:

    you can also add any subset of the following arguments to the signature (the names must match):

    [...]

    • field: the field being validated. Type of object is pydantic.fields.ModelField.

    So you can write a catch-all validator and pass it the ModleField instance as an argument. Then check the type of the field and apply whatever validation you want.

    Very simple example:

    from typing import Any
    
    from pydantic import BaseModel, validator
    from pydantic.fields import ModelField
    
    
    class GeneralModel(BaseModel):
        @validator("*")
        def ensure_non_negative(cls, v: Any, field: ModelField) -> Any:
            if field.type_ is float and v < 0:
                raise ValueError(f"`{field.name}` value must not be negative")
            return v
    
        class Config:
            ...
    

    Usage:

    from pydantic import ValidationError
    
    # ... import GeneralModel
    
    
    class ConcreteModel(GeneralModel):
        x: float
        y: str = "foo"
    
    
    print(ConcreteModel(x=3.14))
    try:
        ConcreteModel(x=-1)
    except ValidationError as err:
        print(err.json(indent=4))
    

    Output:

    x=3.14 y='foo'
    
    [
        {
            "loc": [
                "x"
            ],
            "msg": "`x` value must not be negative",
            "type": "value_error"
        }
    ]
    

    Note: When defining a field as for example list[str], Pydantic internally considers the field type to be str, but its shape to be pydantic.fields.SHAPE_LIST. Luckily, shape is also a public attribute of ModelField. See this answer for an example, where this is important.