3.6. Pydantic Validator

  • Source: https://pydantic-docs.helpmanual.io/usage/validators/

  • validators are "class methods", so the first argument value they receive is the UserModel class, not an instance of UserModel.

  • the second argument is always the field value to validate; it can be named as you please

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

  • values: a dict containing the name-to-value mapping of any previously-validated fields

  • config: the model config

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

  • kwargs: if provided, this will include the arguments above not explicitly listed in the signature

  • validators should either return the parsed value or raise a ValueError, TypeError, or AssertionError (assert statements may be used).

3.6.1. Validator

  • Validation is done in the order fields are defined. E.g. in the example above, password2 has access to password1 (and name), but password1 does not have access to password2. See Field Ordering for more information on how fields are ordered

  • If validation fails on another field (or that field is missing) it will not be included in values, hence if 'password1' in values and ... in this example.

>>> from pydantic import BaseModel, ValidationError, field_validator
>>>
>>>
>>> class UserModel(BaseModel):
...     name: str
...     username: str
...     password1: str
...     password2: str
...
...     @field_validator('name')
...     def name_must_contain_space(cls, v):
...         if ' ' not in v:
...             raise ValueError('must contain a space')
...         return v.title()
...
...     @field_validator('password2')
...     def passwords_match(cls, v, values, **kwargs):
...         if 'password1' in values and v != values['password1']:
...             raise ValueError('passwords do not match')
...         return v
...
...     @field_validator('username')
...     def username_alphanumeric(cls, v):
...         assert v.isalnum(), 'must be alphanumeric'
...         return v
>>>
>>>
>>> user = UserModel(  
...     name='samuel colvin',
...     username='scolvin',
...     password1='zxcvbn',
...     password2='zxcvbn',
... )
>>> print(user)  
name='Samuel Colvin' username='scolvin' password1='zxcvbn' password2='zxcvbn'
>>>
>>> try:  
...     UserModel(
...         name='samuel',
...         username='scolvin',
...         password1='zxcvbn',
...         password2='zxcvbn2',
...     )
... except ValidationError as e:
...     print(e)
2 validation errors for UserModel
name
  must contain a space (type=value_error)
password2
  passwords do not match (type=value_error)

3.6.2. Root Validator

  • Validation performed on the entire model's data.

As with field validators, root validators can have pre=True, in which case they're called before field validation occurs (and are provided with the raw input data), or pre=False (the default), in which case they're called after field validation.

Field validation will not occur if pre=True root validators raise an error. As with field validators, "post" (i.e. pre=False) root validators by default will be called even if prior validators fail; this behaviour can be changed by setting the skip_on_failure=True keyword argument to the validator. The values argument will be a dict containing the values which passed field validation and field defaults where applicable.

from pydantic import BaseModel, ValidationError, model_validator


class UserModel(BaseModel):
    username: str
    password1: str
    password2: str

    @model_validator
    def check_card_number_omitted(cls, values):
        assert 'card_number' not in values, 'card_number should not be included'
        return values

    @model_validator
    def check_passwords_match(cls, values):
        pw1, pw2 = values.get('password1'), values.get('password2')
        if pw1 is not None and pw2 is not None and pw1 != pw2:
            raise ValueError('passwords do not match')
        return values


print(UserModel(username='scolvin', password1='zxcvbn', password2='zxcvbn'))
#> username='scolvin' password1='zxcvbn' password2='zxcvbn'
try:
    UserModel(username='scolvin', password1='zxcvbn', password2='zxcvbn2')
except ValidationError as e:
    print(e)
    """
    1 validation error for UserModel
    __root__
      passwords do not match (type=value_error)
    """

try:
    UserModel(
        username='scolvin',
        password1='zxcvbn',
        password2='zxcvbn',
        card_number='1234',
    )
except ValidationError as e:
    print(e)
    """
    1 validation error for UserModel
    __root__
      card_number should not be included (type=assertion_error)
    """

3.6.3. Pre and Per-item Validator

from typing import List
from pydantic import BaseModel, ValidationError, field_validator


class DemoModel(BaseModel):
    square_numbers: List[int] = []
    cube_numbers: List[int] = []

    # '*' is the same as 'cube_numbers', 'square_numbers' here:
    @field_validator('*', pre=True)
    def split_str(cls, v):
        if isinstance(v, str):
            return v.split('|')
        return v

    @field_validator('cube_numbers', 'square_numbers')
    def check_sum(cls, v):
        if sum(v) > 42:
            raise ValueError('sum of numbers greater than 42')
        return v

    @field_validator('square_numbers', each_item=True)
    def check_squares(cls, v):
        assert v ** 0.5 % 1 == 0, f'{v} is not a square number'
        return v

    @field_validator('cube_numbers', each_item=True)
    def check_cubes(cls, v):
        # 64 ** (1 / 3) == 3.9999999999999996 (!)
        # this is not a good way of checking cubes
        assert v ** (1 / 3) % 1 == 0, f'{v} is not a cubed number'
        return v


print(DemoModel(square_numbers=[1, 4, 9]))
#> square_numbers=[1, 4, 9] cube_numbers=[]
print(DemoModel(square_numbers='1|4|16'))
#> square_numbers=[1, 4, 16] cube_numbers=[]
print(DemoModel(square_numbers=[16], cube_numbers=[8, 27]))
#> square_numbers=[16] cube_numbers=[8, 27]
try:
    DemoModel(square_numbers=[1, 4, 2])
except ValidationError as e:
    print(e)
    """
    1 validation error for DemoModel
    square_numbers -> 2
      2 is not a square number (type=assertion_error)
    """

try:
    DemoModel(cube_numbers=[27, 27])
except ValidationError as e:
    print(e)
    """
    1 validation error for DemoModel
    cube_numbers
      sum of numbers greater than 42 (type=value_error)
    """