3

I have a List in a pydantic model. I'd like my custom validator to run when the list changes (not only on assignment).

from typing import List
from pydantic import BaseModel, validator

class A(BaseModel):
    b: List[int] = []

    class Config:
        validate_assignment = True

    @validator("b")
    def positive(cls, v):
        assert all(i > 0 for i in v), f"No negative numbers: {v}"
        return v

a = A()

a.b = [1, 2, -3]  # error

a.b = [1, 2]  # no error
a.b.append(-3)  # no error

I'd like that last append to raise an error.

I'll get an error if i try to recreate the object (as expected)

A(**a.dict())

Even appending a wrong type is allowed. Why doesn't this break the model?

a.b.append("asdf")  # no error

This is similar/an extension to: How to validate a pydantic object after editing it

Paal Braathen
  • 178
  • 1
  • 11
  • 1
    It looks like you're not the first one who ran into this [limitation](https://github.com/samuelcolvin/pydantic/issues/496). – Paul P May 31 '21 at 10:56
  • 1
    One hint, Pydantic offers a number of [Constrained Types](https://pydantic-docs.helpmanual.io/usage/types/#constrained-types), including `conint`. You can avoid having to write your own validator in this case and simply use `conint(ge=0)` instead of `int` in the model definition. – Paul P May 31 '21 at 10:58
  • 1
    @p3j4p5 That issue look very relevant, thanks. And also thank you very much for the `conint` tip, even though that wasn't my primary question :) – Paal Braathen Jun 03 '21 at 18:41

2 Answers2

3
from pydantic import BaseModel, validator
from typing import List


class PositiveIntList(BaseModel):
    __root__: List[int] = []

    def append(self, value: int) -> None:
        self.__root__.append(value)
        super().__init__(__root__=self.__root__)

    def __getitem__(self, item: int) -> int:
        return self.__root__[item]

    def __setitem__(self, item: int, value: int) -> None:
        self.__root__[item] = value
        super().__init__(__root__=self.__root__)

    @validator("__root__", each_item=True)
    def positive(cls, v):
        assert v > 0, f"No negative numbers: {v}"
        return v


class A(BaseModel):
    b: PositiveIntList = PositiveIntList()


a = A(b=[1, 2, 3])
a = A(b=[1, 2, -3])  # error

a.b = PositiveIntList.parse_obj([4, 5])
a.b = PositiveIntList.parse_obj([4, -5])  # error

a.b.append(6)
a.b.append(-6)  # error

a.b[0] = 7
a.b[0] = -7  # error
YeJun
  • 309
  • 1
  • 6
0

I suggest two ways to evaluate the list, one is to use a validator to run when the list changes and the other is to use the field option as follows:

from typing import List
from pydantic import BaseModel, validator, Field

class A(BaseModel):
    b: List[int] = []

    class Config:
        validate_assignment = True

    @validator("b")
    def positive(cls, v):
        assert all(i > 0 for i in v), f"No negative numbers: {v}"
        return v

class A(BaseModel):
     b: List[int] = Field(ge=0, le=6, unique_items=True,description="")