Validation of field assignment inside validator of pydantic model

Question:

I’ve following pydantic model defined. When I run p = IntOrStr(value=True), I expect failure as True is boolean and it should fail the assignments to __int and __str

class IntOrStr(BaseModel):
    __int: Optional[conint(strict=True, le=100, ge=10)] = None
    __str: Optional[constr(strict=True, max_length=64, min_length=10)] = None
    value: Any

    @validator("value")
    def value_must_be_int_or_str(cls, v):
        try:
            __int = v # no validation. not sure why?
            return v
        except ValidationError as e:
            print(str(e))

        try:
            __str = v # no validation. not sure why?
            return v
        except ValidationError as e:
            print(str(e))

        raise ValueError("error. value must be int or str")

    class Config:
        validate_assignment = True

Does anyone know why __int = v and __str = v do not trigger any validation?

Thanks.

Asked By: Kenny Davidson

||

Answers:

There are a quite a few problems here.

Namespace

This is unrelated to Pydantic; this is just a misunderstanding of how Python namespaces work:

In the namespace of the method, __int and __str are just local variables. What you are doing is simply creating these variables and assigning values to them, then discarding them without doing anything with them.

They are completely unrelated to the fields/attributes of your model.

If you wanted to assign a value to a class attribute, you would have to do the following:

class Foo:
    x: int = 0
    @classmethod
    def method(cls) -> None:
        cls.x = 42

But that is not what you want in this case because…

Class vs. instance

A validator is a class method. This is hinted at by the first parameter being named cls even though the @classmethod decorator can be omitted with @validator.

Thus, you will never be able to assign a value to any field of the model instance inside a validator, regardless of the validate_assignment configuration. The validator merely processes the value provided for assignment to the instance. What it returns may then eventually be assigned to the instance, if no other validators get in the way.

If you want the value passed to one field to influence what is eventually assigned to other fields, you should instead use a @root_validator.

Validator precedence

You need to take into account the order in which validators are called. That order is determined by the order in which fields were defined. (see docs)

Root validators are called after field validators by default. Thus, if you want changes done by a root validator to influence field validation, you need to use pre=True on it.

Underscores

Pydantic does not treat attributes, whose names start with an underscore, as fields, meaning they are not subject to validation. If you need a field name that starts with an underscore, you will have to use an alias.

Working example

All together, I guess you would need something more like the following:

from typing import Any, Optional
from pydantic import BaseModel, Field, ValidationError, conint, constr, root_validator


class IntOrStr(BaseModel):
    a: Optional[
        conint(strict=True, le=100, ge=10)
    ] = Field(default=None, alias="__a")
    b: Optional[
        constr(strict=True, max_length=64, min_length=10)
    ] = Field(default=None, alias="__b")
    value: Any

    @root_validator(pre=True)
    def value_to_a_and_b(cls, values: dict[str, Any]) -> dict[str, Any]:
        value = values.get("value")
        values["__a"] = value
        values["__b"] = value
        return values


if __name__ == "__main__":
    try:
        IntOrStr(value=True)
    except ValidationError as e:
        print(e)

The output:

2 validation errors for IntOrStr
__a
  value is not a valid integer (type=type_error.integer)
__b
  str type expected (type=type_error.str)

Note that in this setup, the errors are actually picked up by the individual default field validators for the conint and constr types.

Also, in this simple example you would not be able to manually set __a or __b because the values would always be overridden in the root validator. But since I don’t know your actual intention, I just set it up like this to trigger your desired validation error.

Hope this helps.

Answered By: Daniil Fajnberg
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.