Allow `SkipValidation` to be applied in any order
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
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.
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?
That is because internally @field_validator's are converted to the class-based approach (AfterValidator), and applied last as documented here.
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.
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 –
SkipValidationmust 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.