pydantic / pydantic

Data validation using Python type hints
https://docs.pydantic.dev
MIT License
20.53k stars 1.84k forks source link

Revalidation of `model_validator(mode="after")` with `model_construct` in nested structures #6978

Open lord-haffi opened 1 year ago

lord-haffi commented 1 year ago

Initial Checks

Description

Initially I thought it's more general and created a discussion here: #6975 But after playing around a bit more I discovered, that this issue appears only when using model_validator(mode="after"). So I believe that this is a bug rather than intended behaviour.

I think the code example below explains the issue better than text. Notice that the validation does not fail because a is not defined (I used model_construct to intentionally bypass it) which is good. But it fails because the custom validator of A (which is constructed) is executed by B "again" - which is bad. At least for my unittests ^^

Example Code

from pydantic import BaseModel
from typing import Self

class A(BaseModel):
    a: float

    @model_validator(mode="after")
    def check_a(self) -> Self:
        raise ValueError("This is a test")

class B(BaseModel):
    b: A

a = A.model_construct()
# This passes
B(b=a)
# Expected behaviour: Simply pass because B is valid
# Actual behaviour: This throws a ValidationError:
# pydantic_core._pydantic_core.ValidationError: 1 validation error for B
# b
#   Value error, This is a test [type=value_error, input_value=A(), input_type=A]
#     For further information visit https://errors.pydantic.dev/2.1/v/value_error

Python, Pydantic & OS Version

> python -c "import pydantic.version; print(pydantic.version.version_info())"
             pydantic version: 2.1.1
        pydantic-core version: 2.4.0
          pydantic-core build: profile=release pgo=false mimalloc=true
                 install path: C:\Users\username\PycharmProjects\TestProject\venv\Lib\site-packages\pydantic
               python version: 3.11.0 (main, Oct 24 2022, 18:26:48) [MSC v.1933 64 bit (AMD64)]
                     platform: Windows-10-10.0.22621-SP0
     optional deps. installed: ['typing-extensions']

Selected Assignee: @samuelcolvin

samuelcolvin commented 1 year ago

Thanks for reporting, as you suggested this is unique to model "after" validators.

The reason is that model after validators are applied outside the model, rather than between the model and the model fields like model "before" validators.

So model after validators look something like:

modelAfterValidator(
    function=check_a,
    validator=modelValidator(
        fieldsValidator(...)
    )
)

Whereas "before" model validators look more like

modelValidator(
    validator=modelBeforeValidator(
        function=check_a,
        validator=fieldsValidator(...)
    )
)

You can see this if you (pretty)print B.__pydantic_validator__.

Unfortunately I don't see an easy way to solve/change this without significantly impacting performance and/or breaking existing behaviour.

What's your use case, may there's another way to get the behaviour you need?

lord-haffi commented 1 year ago

Thanks for your reply! In my case I should be able to do this with mode="before". With mode="after" I just would have saved some instance checks - luckily there are no colliding validators getting in the way. I think that there may be situations in which it could be quite difficult though. But for now and for me it has no priority.

Btw, thanks for your explanation :) Was trying to get trough the code but with the new Rust-core (I'm entirely new to Rust) I was a bit lost ^^

samuelcolvin commented 1 year ago

worth noting that wrap validators probably perform the same as after validators.

I'll leave this open in case other run into it - there may be some clever way for pydantic-core to detect a validator directly wrapping a model and applying the logic used by model validators, but I can't promise anything.