Validators
Custom validation and complex relationships between objects can be achieved using the validator
decorator.
from pydantic import BaseModel, ValidationError, validator class UserModel(BaseModel): name: str username: str password1: str password2: str @validator('name') def name_must_contain_space(cls, v): if ' ' not in v: raise ValueError('must contain a space') return v.title() @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 @validator('username') def username_alphanumeric(cls, v): assert v.isalpha(), 'must be alphanumeric' return v print(UserModel(name='samuel colvin', username='scolvin', password1='zxcvbn', password2='zxcvbn')) #> UserModel 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) """
(This script is complete, it should run "as is")
A few things to note on validators:
- validators are "class methods", the first value they receive here will be the
UserModel
not an instance ofUserModel
- their signature can be
(cls, value)
or(cls, value, values, config, field)
. Any subset ofvalues
,config
andfield
is also permitted, eg.(cls, value, field)
, however due to the way validators are inspected, the variadic key word argument ("**kwargs
") must be calledkwargs
. - validators should either return the new value or raise a
ValueError
,TypeError
, orAssertionError
(assert
statements may be used).
Warning
If you make use of assert
statements, keep in mind that running
Python with the -O
optimization flag
disables assert
statements, and validators will stop working.
-
where validators rely on other values, you should be aware that:
-
Validation is done in the order fields are defined, eg. here
password2
has access topassword1
(andname
), butpassword1
does not have access topassword2
. 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
, henceif 'password1' in values and ...
in this example.
-
Pre and per-item validators🔗
Validators can do a few more complex things:
from typing import List from pydantic import BaseModel, ValidationError, validator class DemoModel(BaseModel): square_numbers: List[int] = [] cube_numbers: List[int] = [] @validator('*', pre=True) # '*' is same as 'cube_numbers', 'square_numbers' here def split_str(cls, v): if isinstance(v, str): return v.split('|') return v @validator('cube_numbers', 'square_numbers') def check_sum(cls, v): if sum(v) > 42: raise ValueError(f'sum of numbers greater than 42') return v @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 @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])) # > DemoModel square_numbers=[1, 4, 9] cube_numbers=[] print(DemoModel(square_numbers='1|4|16')) # > DemoModel square_numbers=[1, 4, 16] cube_numbers=[] print(DemoModel(square_numbers=[16], cube_numbers=[8, 27])) # > DemoModel 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) """
(This script is complete, it should run "as is")
A few more things to note:
- a single validator can apply to multiple fields, either by defining multiple fields or by the special value
'*'
which means that validator will be called for all fields. - the keyword argument
pre
will cause validators to be called prior to other validation - the
each_item
keyword argument will mean validators are applied to individual values (eg. ofList
,Dict
,Set
etc.) not the whole object
Validate Always🔗
For performance reasons by default validators are not called for fields where the value is not supplied. However there are situations where it's useful or required to always call the validator, e.g. to set a dynamic default value.
from datetime import datetime from pydantic import BaseModel, validator class DemoModel(BaseModel): ts: datetime = None @validator('ts', pre=True, always=True) def set_ts_now(cls, v): return v or datetime.now() print(DemoModel()) # > DemoModel ts=datetime.datetime(2017, 11, 8, 13, 59, 11, 723629) print(DemoModel(ts='2017-11-08T14:00')) # > DemoModel ts=datetime.datetime(2017, 11, 8, 14, 0)
(This script is complete, it should run "as is")
You'll often want to use this together with pre
since otherwise with always=True
pydantic would try to validate the default None
which would cause an error.
Root Validators🔗
Validation can also be performed on the entire model's data.
from pydantic import BaseModel, ValidationError, root_validator class UserModel(BaseModel): username: str password1: str password2: str @root_validator(pre=True) def check_card_number_omitted(cls, values): assert 'card_number' not in values, 'card_number should not be included' return values @root_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')) #> UserModel 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) """
(This script is complete, it should run "as is")
As with field validators, root validators can be pre=True
in which case they're called before field
validation occurs 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" root validators raise an error. As with field validators,
"post" (e.g. non pre
) root validators will be called even if field validation fails; the values
argument will
be a dict containing the values which passed field validation and field defaults where applicable.
Field Checks🔗
On class creation validators are checked to confirm that the fields they specify actually exist on the model.
Occasionally however this is not wanted: when you define a validator to validate fields on inheriting models.
In this case you should set check_fields=False
on the validator.
Dataclass Validators🔗
Validators also work in Dataclasses.
from datetime import datetime from pydantic import validator from pydantic.dataclasses import dataclass @dataclass class DemoDataclass: ts: datetime = None @validator('ts', pre=True, always=True) def set_ts_now(cls, v): return v or datetime.now() print(DemoDataclass()) # > DemoDataclass(ts=datetime.datetime(2019, 4, 2, 18, 1, 46, 66149)) print(DemoDataclass(ts='2017-11-08T14:00')) # > DemoDataclass ts=datetime.datetime(2017, 11, 8, 14, 0
(This script is complete, it should run "as is")