29

I want to validate three model Fields of a Pydantic model. To do this, I am importing root_validator from pydantic, however I am getting the error below:

from pydantic import BaseModel, ValidationError, root_validator
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: cannot import name 'root_validator' from 'pydantic' (C:\Users\Lenovo\AppData\Local\Programs\Python\Python38-32\lib\site-packages\pydantic\__init__.py)

I tried this:

@validator
def validate_all(cls,v,values,**kwargs):

I am inheriting my pydantic model from some common fields parent model. Values showing only parent class fields, but not my child class fields. For example:

class Parent(BaseModel):
    name: str
    comments: str
class Customer(Parent):
    address: str
    phone: str
    
    @validator
    def validate_all(cls,v,values, **kwargs):
         #here values showing only (name and comment) but not address and phone.
Chris
  • 18,724
  • 6
  • 46
  • 80
samba
  • 869
  • 4
  • 12
  • 20

5 Answers5

33

To extend on the answer of Rahul R, this example shows in more detail how to use the pydantic validators.

This example contains all the necessary information to answer your question.

Note, that there is also the option to use a @root_validator, as mentioned by Kentgrav, see the example at the bottom of the post for more details.

import pydantic

class Parent(pydantic.BaseModel):
    name: str
    comments: str

class Customer(Parent):
    address: str
    phone: str

    # If you want to apply the Validator to the fields "name", "comments", "address", "phone"
    @pydantic.validator("name", "comments", "address", "phone")
    @classmethod
    def validate_all_fields_one_by_one(cls, field_value):
        # Do the validation instead of printing
        print(f"{cls}: Field value {field_value}")

        return field_value  # this is the value written to the class field

    # if you want to validate to content of "phone" using the other fields of the Parent and Child class
    @pydantic.validator("phone")
    @classmethod
    def validate_one_field_using_the_others(cls, field_value, values, field, config):
        parent_class_name = values["name"]
        parent_class_address = values["address"] # works because "address" is already validated once we validate "phone"
        # Do the validation instead of printing
        print(f"{field_value} is the {field.name} of {parent_class_name}")

        return field_value 

Customer(name="Peter", comments="Pydantic User", address="Home", phone="117")

Output

<class '__main__.Customer'>: Field value Peter
<class '__main__.Customer'>: Field value Pydantic User
<class '__main__.Customer'>: Field value Home
<class '__main__.Customer'>: Field value 117
117 is the phone number of Peter
Customer(name='Peter', comments='Pydantic User', address='Home', phone='117')

To answer your question in more detail:

Add the fields to validate to the @validator decorator directly above the validation function.

  • @validator("name") uses the field value of "name" (e.g. "Peter") as input to the validation function. All fields of the class and its parent classes can be added to the @validator decorator.
  • the validation function (validate_all_fields_one_by_one) then uses the field value as the second argument (field_value) for which to validate the input. The return value of the validation function is written to the class field. The signature of the validation function is def validate_something(cls, field_value) where the function and variable names can be chosen arbitrarily (but the first argument should be cls). According to Arjan (https://youtu.be/Vj-iU-8_xLs?t=329), also the @classmethod decorator should be added.

If the goal is to validate one field by using other (already validated) fields of the parent and child class, the full signature of the validation function is def validate_something(cls, field_value, values, field, config) (the argument names values,field and config must match) where the value of the fields can be accessed with the field name as key (e.g. values["comments"]).

Edit1: If you want to check only input values of a certain type, you could use the following structure:

@validator("*") # validates all fields
def validate_if_float(cls, value):
    if isinstance(value, float):
        # do validation here
    return value

Edit2: Easier way to validate all fields together using @root_validator:

import pydantic

class Parent(pydantic.BaseModel):
    name: str
    comments: str

class Customer(Parent):
    address: str
    phone: str

    @pydantic.root_validator()
    @classmethod
    def validate_all_fields_at_the_same_time(cls, field_values):
        # Do the validation instead of printing
        print(f"{cls}: Field values are: {field_values}")
        assert field_values["name"] != "invalid_name", f"Name `{field_values['name']}` not allowed."
        return field_values

Output:

Customer(name="valid_name", comments="", address="Street 7", phone="079")
<class '__main__.Customer'>: Field values are: {'name': 'valid_name', 'comments': '', 'address': 'Street 7', 'phone': '079'}
Customer(name='valid_name', comments='', address='Street 7', phone='079')
Customer(name="invalid_name", comments="", address="Street 7", phone="079")
ValidationError: 1 validation error for Customer
__root__
  Name `invalid_name` not allowed. (type=assertion_error)
wueli
  • 951
  • 11
  • 19
  • How would you go about validating all fields of a certain type? I know there is a `*` input to the @validator() decorator that will use the validator for all fields. However, I'd like to use a specific validator to validate all floats for example. – Curtwagner1984 Jan 02 '22 at 10:24
  • 1
    Since Python is dynamically typed, I think you need the validator for all fields `@validator("*")` and make a type check inside with `isinstance(value, float)`. If the type is `float`, do your validation, otherwise, return the input argument return value. However, this validation will activate for all inputs of type float, independent of the types in the class declaration. – wueli Jan 02 '22 at 16:40
  • Thank you this makes sense – Curtwagner1984 Jan 02 '22 at 17:44
  • 2
    I don't think you need the `@classmethod` decorator because `@validator` already returns a classmethod; see [this issue](https://github.com/pydantic/pydantic/issues/1415). – Rich Inman Sep 30 '22 at 00:30
9

You need to pass the fields as arguments of the decorator.

class Parent(BaseModel):
    name: str
    comments: str

class Customer(Parent):
    address: str
    phone: str

    @validator("name", "coments", "address", "phone")
    def validate_all(cls, v, values, **kwargs):
Nuno André
  • 4,739
  • 1
  • 33
  • 46
Rahul R
  • 151
  • 1
  • 6
  • 24
    This would be a better answer if you explained how the code you provided answers the question. – pppery Jun 24 '20 at 14:07
5

First off, if you are having an error importing root_validator, I would update pydantic.

pip install -U pydantic

A lot of the examples above show you how to use the same validator on multiple values one at a time. Or they add a lot of unnecessary complexity to accomplish what you want. You can simply use the following code to validate multiple fields at the same time in the same validator using the root_validator decorator.:

from pydantic import root_validator
from pydantic import BaseModel

class Parent(BaseModel):
    name: str = "Peter"
    comments: str = "Pydantic User"

class Customer(Parent):
    address: str = "Home"
    phone: str = "117"

    @root_validator
    def validate_all(cls, values):
         print(f"{values}")
         values["phone"] = "111-111-1111"
         values["address"] = "1111 Pydantic Lane"
         print(f"{values}")
         return values

Output:

{'name': 'Peter', 'comments': 'Pydantic User', 'address': 'Home', 'phone': '117'}

{'name': 'Peter', 'comments': 'Pydantic User', 'address': '1111 Pydantic Lane', 'phone': '111-111-1111'}
Kentgrav
  • 259
  • 3
  • 4
2

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, ValidationError
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

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, ValidationError
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
Chris
  • 18,724
  • 6
  • 46
  • 80
-1

This example contains all the necessary information to answer your question.

    class User(BaseModel):
        name: Optional[str] = ""

        class Config:
            validate_assignment = True

        @validator("name")
            def set_name(cls, name):
            return name or "foo"
  • 3
    As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Nov 16 '21 at 11:31