Lucien Chardon
Lucien Chardon

Reputation: 301

Run pydantic validators for annotated types on instantiation of type, not on assignment to BaseModel field

I have a project using fastapi and pydantic. I want to define certain subtypes of standard library types, eg:

def check_negative(v: int) -> int:
    assert v >= 0, f"{v} is negative"
    return v

PositiveInt = Annotated[int, AfterValidator(check_negative)]

class DemoModel(BaseModel):
    numbers: list[PositiveInt]

ISSUE:

The following code runs the evaluation only when the list of numbers is passed on instantiation of the DemoModel. Meaning numbers contains false values until it is actually used in a BaseModel. This is unconvenient since i want to evaluate the value, for example, directly in the route of a defined fastapi endpoint and not wait until the value is passed down to the model.

numbers = [PositiveInt(2), PositiveInt(-3)] <--- should throw evaluation error here
print(numbers)
demo = DemoModel(numbers=numbers) <--- throws evaluation error here

QUESTION:

Is there a way to run the validations like AfterValidator directly on the instantiation of the type PositiveInt and not just when it is used for the instantiation of a BaseModel class?

So that in the given example, the validation error would be thrown in the first line numbers = [PositiveInt(2), PositiveInt(-3)] and not at the end at demo = DemoModel(numbers=numbers).

Thanks for your help!

Upvotes: 0

Views: 2250

Answers (2)

Zdeněk Softić
Zdeněk Softić

Reputation: 36

You could use TypeAdapter like this:

from pydantic import TypeAdapter

TypeAdapter(list[PositiveInt]).validate_python([2, -3])

Python docs on type adapter: https://docs.pydantic.dev/latest/api/type_adapter/

Upvotes: 2

Lucien Chardon
Lucien Chardon

Reputation: 301

Not sure if this is the best approach, but this is how i got it working now, after reading this part of the pydantic docs:

class PositiveInt(int):
    def __new__(cls, number: int):
        check_negative(number)
        return super().__new__(cls, number)

    @classmethod
    def __get_pydantic_core_schema__(
        cls, source: Type[Any], handler: GetCoreSchemaHandler
    ) -> core_schema.CoreSchema:
        return core_schema.general_after_validator_function(
            cls.validate,
            core_schema.int_schema(),
            serialization=core_schema.int_schema(),
        )

    @classmethod
    def validate(cls, v: int, _info):
        return check_negative(v)


class DemoModel(BaseModel):
    numbers: list[PositiveInt]

This gives me assertion errors right on the instantiation of PositiveInt AND i can use it in DemoModel as well.

if __name__ == "__main__":
    a = PositiveInt(1)
    b = PositiveInt(-3)

    c = DemoModel(numbers=[PositiveInt(1), PositiveInt(-3)])

Upvotes: 1

Related Questions