Search code examples
pythonvalidationpydanticsqlmodel

Pydantic root_validator unexpected behaviour


I have a file test.py like this:

from pydantic import root_validator
from sqlmodel import SQLModel
from typing import List

from devtools import debug

class Base(SQLModel):

    @root_validator(pre=True)
    def validate(cls, values):
        debug(values, type(values))
        for k, v in values.items():
            # here we perform the validation
            # for this example we do nothing
            pass
        debug("EXIT")
        return values

class A(Base):
    id: int
    name: str

class B(Base):
    id: int
    others: List[A]

Now if I do

$ python3

Python 3.10.5 (v3.10.5:f377153967, Jun  6 2022, 12:36:10) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license" for more information.

>>> from test import A
>>> from test import B
>>> x = A(id=1, name="john")

test.py:11 Base.validate
    values: {
        'id': 1,
        'name': 'john',
    } (dict) len=2
    type(values): <class 'dict'> (type)
test.py:16 Base.validate
    'EXIT' (str) len=4

>>> y = B(id=42, others=[])

test.py:11 Base.validate
    values: {
        'id': 42,
        'others': [],
    } (dict) len=2
    type(values): <class 'dict'> (type)
test.py:16 Base.validate
    'EXIT' (str) len=4

>>> z = B(id=100, others=[x])

test.py:11 Base.validate
    values: {
        'id': 42,
        'others': [
            A(
                id=1,
                name='john',
            ),
        ],
    } (dict) len=2
    type(values): <class 'dict'> (type)
test.py:16 Base.validate
    'EXIT' (str) len=4
test.py:11 Base.validate
    values: A(
        id=1,
        name='john',
    ) (A)
    type(values): <class 'test.A'> (SQLModelMetaclass)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/yuri/Desktop/exact/fractal/fractal-common/venv/lib/python3.10/site-packages/sqlmodel/main.py", line 498, in __init__
    values, fields_set, validation_error = validate_model(
  File "pydantic/main.py", line 1077, in pydantic.main.validate_model
  File "pydantic/fields.py", line 895, in pydantic.fields.ModelField.validate
  File "pydantic/fields.py", line 928, in pydantic.fields.ModelField._validate_sequence_like
  File "pydantic/fields.py", line 1094, in pydantic.fields.ModelField._validate_singleton
  File "pydantic/fields.py", line 884, in pydantic.fields.ModelField.validate
  File "pydantic/fields.py", line 1101, in pydantic.fields.ModelField._validate_singleton
  File "pydantic/fields.py", line 1148, in pydantic.fields.ModelField._apply_validators
  File "pydantic/class_validators.py", line 318, in pydantic.class_validators._generic_validator_basic.lambda13
  File "/Users/yuri/Desktop/exact/fractal/fractal-common/test.py", line 12, in validate
    for k, v in values.items():
AttributeError: 'A' object has no attribute 'items'

I don't undestand what is happening with z. Why after calling the validator for class B, the constructor call the validator also for class A but this time values is not a dict but a <class 'test.A'> (SQLModelMetaclass).


Solution

  • This has nothing to do with SQLModel per se, but is an issue that SQLModel inherits from Pydantic's BaseModel.

    You are breaking regular model validation by overriding the BaseModel.validate method. It serves a special purpose. When you have a model field annotated with another model, like your B.others being of type list[A], the outer (B) model will call the inner (A) model's validate method.

    That method is designed as a "normal" validator method, i.e. the value to be validated will be passed to it as the first positional argument. In this case that value is your A model instance x that you passed for in the list for others. That is why it shows <class 'test.A'> as the type of values. (Don't know why it mentions the metaclass as well, but I guess that is just what this debug function does.)

    The solution is very simple: Name your root validator method something else that does not override a built-in method of BaseModel. This is also a good idea from a semantic perspective because validate is not very descriptive. Try and indicate what it does on top of normal model validation in its name.

    But even just changing the method name to foo will fix your error.