Pydantic: validating a nested model

Viewed 5925

I have a nested model in Pydantic. I have a root_validator function in the outer model. I was under the impression that if the outer root validator is called, then the inner model is valid. But apparently not. In the following MWE, I give the wrong field name to the inner model, but the outer validator is failing:

from typing import List
from pydantic import BaseModel
from pydantic.class_validators import root_validator

class CarList(BaseModel):
    cars: List[str]
    colors: List[str]

class CarDealership(BaseModel):
    name: str
    cars: CarList

    @root_validator()
    def check_length(cls, v):
        cars_list = v.get("cars")

        if len(cars_list.cars) != len(cars_list.colors):
            raise ValueError("wrong length")
        
        return v


car_deal = {
    "cars": {
        "cars1": ["toyota", "tesla"], 
        "colors": ["white", "red"]
    }
}

CarDealership.parse_obj(car_deal)

The error I get is:

  File "test.py", line 17, in check_length
    if len(cars_list.cars) != len(cars_list.colors):
AttributeError: 'NoneType' object has no attribute 'cars'

I was expecting something like:

pydantic.error_wrappers.ValidationError: 1 validation error for CarList
cars
  field required (type=value_error.missing)

How can I make sure the inner model is validated first?

3 Answers

Update:validation on the outer class version

from typing import List
from pydantic import BaseModel
from pydantic.class_validators import root_validator


class CarList(BaseModel):
    cars: List[str]
    colors: List[str]


class CarDealership(BaseModel):
    name: str
    cars_list: CarList

    @root_validator(pre=True)
    def check_length(cls, v):
        cars_list = v.get("cars_list")

        if not cars_list:
            return v
        cars = cars_list.get("cars")
        colors = cars_list.get("colors")
        if not isinstance(cars, list) or not isinstance(colors, list):
            return v
        if cars is not None and colors is not None and len(cars) != len(colors):
            raise ValueError("wrong length")

        return v

The root_validator default pre=False,the inner model has already validated,so you got v == {}.

You can make check_length in CarList,and check whether cars and colors are exist(they has has already validated, if failed will be None).

from typing import List
from pydantic import BaseModel
from pydantic.class_validators import root_validator


class CarList(BaseModel):
    cars: List[str]
    colors: List[str]

    @root_validator
    def check_length(cls, v):
        cars = v.get("cars")
        colors = v.get("colors")

        if cars is not None and colors is not None and len(cars) != len(colors):
            raise ValueError("wrong length")

        return v


class CarDealership(BaseModel):
    name: str
    car_list: CarList


car_deal_1 = {
    "name": "value",
    "car_list": {
        "cars1": ["toyota", "tesla"],
        "colors": ["white", "red"]
    }
}

CarDealership.parse_obj(car_deal_1)
# pydantic.error_wrappers.ValidationError: 1 validation error for CarDealership
# car_list -> cars
#   field required (type=value_error.missing)

car_deal_2 = {
    "name": "value",
    "car_list": {
        "cars": ["toyota", "tesla"],
        "colors": ["white", "red", "bule"]
    }
}

CarDealership.parse_obj(car_deal_2)
# pydantic.error_wrappers.ValidationError: 1 validation error for CarDealership
# car_list -> __root__
#   wrong length (type=value_error)

Based on @YeJun response, but assuming your comment to the response that you need to use the inner class for other purposes, you can create an intermediate class with the validation while keeping the original CarList class for other uses:

from typing import List
from pydantic import BaseModel
from pydantic.class_validators import root_validator


class CarList(BaseModel):
    cars: List[str]
    colors: List[str]


# this class will have the same fields from CarList, but with validation
class CarListWithValidator(CarList):
    @root_validator
    def check_length(cls, v):
        cars = v.get("cars")
        colors = v.get("colors")

        if cars is not None and colors is not None and len(cars) != len(colors):
            raise ValueError("wrong length")

        return v


class CarDealership(BaseModel):
    name: str
    car_list: CarListWithValidator


car_deal_2 = {
    "name": "value",
    "car_list": {
        "cars": ["toyota", "tesla"],
        "colors": ["white", "red", "bule"]
    }
}

CarDealership.parse_obj(car_deal_2)
# pydantic.error_wrappers.ValidationError: 1 validation error for CarDealership
# car_list -> __root__
#   wrong length (type=value_error)

This is how it would work in convtools models (docs | github):

from typing import List
from convtools.contrib.models import DictModel, build


class CarList(DictModel):
    cars: List[str]
    colors: List[str]

    @classmethod
    def validate(cls, model):
        if len(model.cars) != len(model.colors):
            return None, {"cars_length": "wrong length"}
        return model, None


class CarDealership(DictModel):
    name: str
    cars: CarList


car_deal = {"cars": {"cars": ["toyota", "tesla"], "colors": ["white"]}}
obj, errors = build(CarDealership, car_deal)

# In [8]: errors
# Out[8]:
# {'name': {'__ERRORS': {'required': True}},
#  'cars': {'__ERRORS': {'cars_length': 'wrong length'}}}


car_deal = {
    "cars": {"cars": ["toyota", "tesla"], "colors": ["white", "red"]},
    "name": "something",
}
obj, errors = build(CarDealership, car_deal)

"""
>>> In [75]: obj
>>> Out[75]: CarDealership(name='something', cars=CarList(cars=['toyota', 'tesla'], colors=['white', 'red']))
"""
Related