pythonvalidationpydanticdatamodelrecursive-datastructures

Validation on recursive models don't raise error even on failing


I am trying to add a validation in pydantic data model which is recursive. However, the validator is not raising an error even when the check fails.

from __future__ import annotations

from typing import Optional, Union, List

import orjson
from pydantic import BaseModel, validator


def orjson_dumps(v, *, default):
    return orjson.dumps(v, default=default).decode()


class Condition(BaseModel):
    operation: str
    value: Union[List[Condition], Condition, float, int, list, str]
    attribute_name: Optional[str]

    @validator("operation", pre=True, always=True)
    @classmethod
    def validate_operations(cls, field_value):
        if field_value not in ["and", "or", "equal"]:
            msg = f"Supported operations: [and, or, equal]. Passed operation: {field_value}"
            raise ValueError(msg)
        return field_value


if __name__ == '__main__':
    condition4 = {'operation': 'or', 'value': [
        {'operation': 'equal', 'attribute_name': 'num_pay_txns_month', 'value': 6},
        {'operation': 'incorrect_operation', 'attribute_name': 'num_pay_txns_week', 'value': 3}]}
    print(Condition(**condition4))

Actual Output:

operation='or' value=[{'operation': 'equal', 'attribute_name': 'num_pay_txns_month', 'value': 6}, {'operation': 'incorrect_operation', 'attribute_name': 'num_pay_txns_week', 'value': 3}] attribute_name=None

Expected output:

ValueError

Solution

  • Ah, this took me a while to figure out. The problem is in the value annotation. It is because you have a non-specified list in that type union. Remove that and the validation error is raised as expected:

    class Condition(BaseModel):
        operation: str
        value: Union[List[Condition], Condition, float, int, str]
        ...
    

    The reason is that by adding the non-specified list you are basically saying that value is allowed to be list[Any]. Thus, when all other attempts to validate that dictionary containing the wrong value fail, it is just taken "as is", i.e. some dictionary. Which is why value on the top-level object is just a list of dictionaries, not of Condition instances.