pydantic icon indicating copy to clipboard operation
pydantic copied to clipboard

Allow `SkipValidation` to be applied in any order

Open jonaskuske opened this issue 8 months ago • 5 comments

Initial Checks

  • [x] I confirm that I'm using Pydantic V2

Description

Edit by @Viicos: see following comment for the actual feature request.

When a field validator exists on a parent model, that validator always runs and does not respect the SkipValidation annotation of the field.

Example Code

import pydantic

class Parent(pydantic.BaseModel):
    @pydantic.field_validator("the_field", check_fields=False)
    @classmethod
    def _validate_the_field(cls, value, info):
        raise Exception("I shouldn't be reached!")

class Child(Parent):
    the_field: pydantic.SkipValidation[int]

Child(the_field=1337)

Python, Pydantic & OS Version

             pydantic version: 2.11.3
        pydantic-core version: 2.33.1
          pydantic-core build: profile=release pgo=false
                 install path: <project>/.venv/lib/python3.13/site-packages/pydantic
               python version: 3.13.1 (main, Jan  8 2025, 08:39:24) [GCC 11.4.0]
                     platform: Linux-5.15.167.4-microsoft-standard-WSL2-x86_64-with-glibc2.39
             related packages: typing_extensions-4.13.1 fastapi-0.115.12 pydantic-extra-types-2.10.3
                       commit: unknown

jonaskuske avatar Apr 29 '25 10:04 jonaskuske

Can be reproduced with:

from pydantic import BaseModel, SkipValidation, field_validator

class Model(BaseModel):
    the_field: SkipValidation[int]

    @field_validator('the_field')
    @classmethod
    def _validate_the_field(cls, value):
        raise Exception("I shouldn't be reached!")

Child(the_field=1337)
#> Exception

SkipValidation is documented as required to be applied last, but this is inelegant. As such, I'm converting to a feature request.

Viicos avatar May 01 '25 09:05 Viicos

Thank you for looking into this! And interesting that it even happens within the same model.

I must say that the docs aren't really clear here though. You say SkipValidation is required to be applied last, but the docs state that it should generally be the final annotation applied to a type, because subsequent annotation-applied transformations might not work.

But...

class Model(BaseModel):
    the_field: Annotated[int, SkipValidation]

    @field_validator('the_field')
    @classmethod
    def _validate_the_field(cls, value):
        raise Exception("I shouldn't be reached!")

...without having a deeper understanding of how Pydantic works under the hood, I'd say in the example above, SkipValidation is the only (and thus last) type annotation?

jonaskuske avatar May 01 '25 14:05 jonaskuske

That is because internally @field_validator's are converted to the class-based approach (AfterValidator), and applied last as documented here.

Viicos avatar May 01 '25 14:05 Viicos

That makes sense, thanks!

But follow-up question then: why does the same thing happen with @field_validator(mode='before')?
Shouldn't the resulting BeforeValidator be added last after the existing metadata, and thus apply before the SkipValidation, as before and wrap validators apply from right to left? (so that SkipValidation would apply last, as is required)

Edit: I think I get it, this is not about the order in which validators are run, but the order in which validators are defined – SkipValidation must appear last in the metadata, not run last? Or put differently, it's about the order in which the user applies the annotation to the type, and not the order in which pydantic applies the validator function to the value.

jonaskuske avatar May 01 '25 15:05 jonaskuske

Edit: I think I get it, this is not about the order in which validators are run, but the order in which validators are defined – SkipValidation must appear last in the metadata, not run last?

This is exactly it!

Side note: we have an https://xkcd.com/1172/ situation here, this is relied on e.g. in https://github.com/pydantic/pydantic/issues/11853.

Viicos avatar May 09 '25 07:05 Viicos