Option 1 - Using the @validator
decorator
As per the documentation, "a single validator
can be applied to multiple fields by passing it multiple field names" (and "can also be called on all fields by passing the special value '*'
"). Thus, you could add the fields you wish to validate to the validator
decorator, and using field.name
attribute you can check which one to validate each time the validator
is called. If a field does not pass the validation, you could raise ValueError
, "which will be caught and used to populate ValidationError
" (see "Note" section here). If you need to validate a field based on other field(s), you have to check first if they have already been validated using values.get()
method, as shown in this answer (Update 2). The below demonstrates an example, where fields such as name
, country_code
, and phone
number (based on the provided country_code
) are validated. The regex patterns provided are just examples for the purposes of this demo, and are based on this and this answer..
from pydantic import BaseModel, validator
import re
name_pattern = re.compile(r'[a-zA-Z\s]+$')
country_codes = {"uk", "us"}
UK_phone_pattern = re.compile(r'^(\+44\s?7\d{3}|\(?07\d{3}\)?)\s?\d{3}\s?\d{3}$') # UK mobile phone number. Valid example: +44 7222 555 555
US_phone_pattern = re.compile(r'^(\([0-9]{3}\) |[0-9]{3}-)[0-9]{3}-[0-9]{4}$') # US phone number. Valid example: (123) 123-1234
phone_patterns = {"uk": UK_phone_pattern, "us": US_phone_pattern}
class Parent(BaseModel):
name: str
comments: str
class Customer(Parent):
address: str
country_code: str
phone: str
@validator('name', 'country_code', 'phone')
def validate_atts(cls, v, values, field):
if field.name == "name":
if not name_pattern.match(v): raise ValueError(f'{v} is not a valid name.')
elif field.name == "country_code":
if not v.lower() in country_codes: raise ValueError(f'{v} is not a valid country code.')
elif field.name == "phone" and values.get('country_code'):
c_code = values.get('country_code').lower()
if not phone_patterns[c_code].match(v): raise ValueError(f'{v} is not a valid phone number.')
return v
Update - Pydantic V2 Example
In Pydantic V2, @validator
has been deprecated, and was replaced by @field_validator
. If you want to access values
from another field inside a @field_validator
, this may be possible using ValidationInfo.data
, which is a dict of field name to field value.
from pydantic import BaseModel, ValidationInfo, field_validator
import re
# ... the rest of the code is the same as above
class Customer(Parent):
address: str
country_code: str
phone: str
@field_validator('name', 'country_code', 'phone')
@classmethod
def validate_atts(cls, v: str, info: ValidationInfo):
if info.field_name == 'name':
if not name_pattern.match(v): raise ValueError(f'{v} is not a valid name.')
elif info.field_name == 'country_code':
if not v.lower() in country_codes: raise ValueError(f'{v} is not a valid country code.')
elif info.field_name == 'phone' and info.data.get('country_code'):
c_code = info.data.get('country_code').lower()
if not phone_patterns[c_code].match(v): raise ValueError(f'{v} is not a valid phone number.')
return v
Option 2 - Using the @root_validator
decorator
Another approach would be using the @root_validator
, which allows validation to be performed on the entire model's data.
from pydantic import BaseModel, root_validator
import re
name_pattern = re.compile(r'[a-zA-Z\s]+$')
country_codes = {"uk", "us"}
UK_phone_pattern = re.compile(r'^(\+44\s?7\d{3}|\(?07\d{3}\)?)\s?\d{3}\s?\d{3}$') # UK mobile phone number. Valid example: +44 7222 555 555
US_phone_pattern = re.compile(r'^(\([0-9]{3}\) |[0-9]{3}-)[0-9]{3}-[0-9]{4}$') # US phone number. Valid example: (123) 123-1234
phone_patterns = {"uk": UK_phone_pattern, "us": US_phone_pattern}
class Parent(BaseModel):
name: str
comments: str
class Customer(Parent):
address: str
country_code: str
phone: str
@root_validator()
def validate_atts(cls, values):
name = values.get('name')
comments = values.get('comments')
address = values.get('address')
country_code = values.get('country_code')
phone = values.get('phone')
if name is not None and not name_pattern.match(name):
raise ValueError(f'{name} is not a valid name.')
if country_code is not None and not country_code.lower() in country_codes:
raise ValueError(f'{country_code} is not a valid country code.')
if phone is not None and country_code is not None:
if not phone_patterns[country_code.lower()].match(phone):
raise ValueError(f'{phone} is not a valid phone number.')
return values
Update - Pydantic V2 Example
In Pydantic V2, @root_validator
has been deprecated, and was replaced by @model_validator
. Model validators can be mode='before'
, mode='after'
or mode='wrap'
. In this case, mode='after'
is suited best. As described in the documentation:
mode='after'
validators are instance methods and always receive an
instance of the model as the first argument. You should not use (cls, ModelType)
as the signature, instead just use (self)
and let type
checkers infer the type of self
for you. Since these are fully type
safe they are often easier to implement than mode='before'
validators. If any field fails to validate, mode='after'
validators
for that field will not be called.
Using mode='after'
from pydantic import BaseModel, model_validator
import re
# ... the rest of the code is the same as above
class Customer(Parent):
address: str
country_code: str
phone: str
@model_validator(mode='after')
def validate_atts(self):
name = self.name
comments = self.comments
address = self.address
country_code = self.country_code
phone = self.phone
if name is not None and not name_pattern.match(name):
raise ValueError(f'{name} is not a valid name.')
if country_code is not None and not country_code.lower() in country_codes:
raise ValueError(f'{country_code} is not a valid country code.')
if phone is not None and country_code is not None:
if not phone_patterns[country_code.lower()].match(phone):
raise ValueError(f'{phone} is not a valid phone number.')
return self
Using mode='before'
In case you would rather using mode='before
, you could this as follows. Note that in this case, you should, however, perform your own checks on whether the field values are in the expected format (e.g., str
in the example below), before moving on with further processing/validation (e.g., converting values to lowercase, string values comparisons, etc.)—not included below.
from pydantic import BaseModel, model_validator
from typing import Any
import re
# ... the rest of the code is the same as above
class Customer(Parent):
address: str
country_code: str
phone: str
@model_validator(mode='before')
@classmethod
def validate_atts(cls, data: Any):
if isinstance(data, dict):
name = data.get('name')
comments = data.get('comments')
address = data.get('address')
country_code = data.get('country_code')
phone = data.get('phone')
if name is not None and not name_pattern.match(name):
raise ValueError(f'{name} is not a valid name.')
if country_code is not None and not country_code.lower() in country_codes:
raise ValueError(f'{country_code} is not a valid country code.')
if phone is not None and country_code is not None:
if not phone_patterns[country_code.lower()].match(phone):
raise ValueError(f'{phone} is not a valid phone number.')
return data
Test Examples for Options 1 & 2
from pydantic import ValidationError
# should throw "Value error, (123) 123-1234 is not a valid phone number."
try:
Customer(name='john', comments='hi', address='some address', country_code='UK', phone='(123) 123-1234')
except ValidationError as e:
print(e)
# should work without errors
print(Customer(name='john', comments='hi', address='some address', country_code='UK', phone='+44 7222 555 555'))
from pydantic import root_validator
raises anImportError
, this is most probably because you do not have the right version ofpydantic
... Which version do you use ? – Alloway