tikka
tikka

Reputation: 577

Trying to set a superclass field in a subclass using validator

I am trying to set a super-class field in a subclass using validator as follows:

Approach 1

from typing import List
from pydantic import BaseModel, validator, root_validator
    
class ClassSuper(BaseModel):
  field1: int = 0

class ClassSub(ClassSuper):
  field2: List[int]

  @validator('field1')
  def validate_field1(cls, v, values):
    return len(values["field2"])

sub = ClassSub(field2=[1, 2, 3])
print(sub.field1)  # It prints 0, but expected it to print 3

If I run the code above it prints 0, but I expected it to print 3 (which is basically len(field2)). However, if I use @root_validator() instead, I get the expected result.

Approach 2

from typing import List
from pydantic import BaseModel, validator, root_validator
    
class ClassSuper(BaseModel):
  field1: int = 0

class ClassSub(ClassSuper):
  field2: List[int]

  @root_validator()
  def validate_field1(cls, values):
    values["field1"] = len(values["field2"])
    return values

sub = ClassSub(field2=[1, 2, 3])
print(sub.field1)  # This prints 3, as expected

New to using pydantic and I am bit puzzled what I am doing wrong with the Approach 1. Thank you for your help.

Upvotes: 3

Views: 855

Answers (1)

Daniil Fajnberg
Daniil Fajnberg

Reputation: 18458

The reason your Approach 1 does not work is because by default, validators for a field are not called, when the value for that field is not supplied (see docs).

Your validate_field1 is never even called. If you add always=True to your @validator, the method is called, even if you don't provide a value for field1.

However, if you try that, you'll see that it will still not work, but instead throw an error about the key "field2" not being present in values.

This in turn is due to the fact that validators are called in the order they were defined. In this case, field1 is defined before field2, which means that field2 is not yet validated by the time validate_field1 is called. And values only contains previously-validated fields (see docs). Thus, at the time validate_field1 is called, values is simply an empty dictionary.

Using the @root_validator is the correct approach here because it receives the entire model's data, regardless of whether or not field values were supplied explicitly or by default.

And just as a side note: If you don't need to specify any parameters for it, you can use @root_validator without the parantheses.

And as another side note: If you are using Python 3.9+, you can use the regular list class as the type annotation. (See standard generic alias types) That means field2: list[int] without the need for typing.List.

Hope this helps.

Upvotes: 3

Related Questions