Search code examples
pythonvalidationpydantic

How can pydantic validator access 2nd attribute?


How can I make a pydantic validator validate input against another class object (not another input arg, but really another object sitting in the class definition, such as a property, or function). --> I would love to call self.xx.

So, I know how to validate one input arg against another input arg (e.g., validating that the color is green if plant is a tree):

from pydantic import BaseModel, validator

class MyClass(BaseModel):

    plant: str
    color: str
   
    @validator('color')
    def tree_is_green(cls, v, values, **kwargs):
        if values['plant'] == 'tree':
            if v != 'green':
                raise ValueError('tree must be green')
        return v

It will bark at blue trees, etc., while roses can be of any color:

my_plant = MyClass(color='blue', plant='rose')      # works
my_plant = MyClass(color='blue', plant='tree')      # fails
my_plant = MyClass(color='green', plant='tree')      # works

So far, so good.

Now, how can I generalize this, and use a property? Say the validator becomes a bit more complicated than just a ==, and I don't want to copy/paste all the functionality of my functions and properties into the validator.

from pydantic import BaseModel, validator

class MyClass(BaseModel):

    plant: str
    color: str

    @property
    def is_tree(self) -> bool:
        return self.plant == 'tree'

    @validator('color')
    def tree_is_green(cls, v, values, **kwargs):
        if values['is_tree'] is True:
            if v != 'green':
                raise ValueError('tree must be green')
        return v
    

The above code fails always, no matter what instance I try to create:

my_plant = MyClass(color='green', plant='tree')
>>> KeyError: 'is_tree'

I tried to replace the values[] by a desperate attempt to cls, but to no avail. Using the below will never return an error - and it's simply not validating anything:

from pydantic import BaseModel, validator

class MyClass(BaseModel):

    plant: str
    color: str

    @property
    def is_tree(self) -> bool:
        return self.plant == 'tree'

    @validator('color')
    def tree_is_green(cls, v, values, **kwargs):
        if cls.is_tree is True:
            if v != 'green':
                raise ValueError('tree must be green')
        return v

leads to unwanted output:

my_plant = MyClass(color='blue', plant='tree')
print(my_plant)
plant='tree' color='blue'

Is there any way to construct a validator that is based on functionality/objects of the class, rather than only the input args, or static variables? Conceptually, it should work, as the property is_tree does not depend on color, so any form of circular reference is excluded.. and if that blew up, I would humbly accept it and think of a better validator..


Solution

  • Since a pydantic validator is a classmethod, it unfortunately won't be able to use the @property as you're expecting. The @property is designed to work on an instance of MyClass, similar to any other instance method; however, during the "validation" stage of pydantic, the instance isn't yet created, and it's calling validators as class methods, so it only has access to other classmethods and class attributes.

    You could switch is_tree to a classmethod and pass it the plant value:

    from pydantic import BaseModel, validator
    
    class MyClass(BaseModel):
    
        plant: str
        color: str
    
        @classmethod
        def is_tree(cls, plant_val) -> bool:
            return plant_val == 'tree'
    
        @validator('color')
        def tree_is_green(cls, v, values):
            if cls.is_tree(values['plant']) and v != 'green':
                    raise ValueError('tree must be green')
            return v
    
    MyClass(color='blue', plant='rose') # no problems
    MyClass(color='green', plant='tree') # no problems
    MyClass(color='blue', plant='tree') # ValidationError: color -> tree must be green