Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow access to field_name and data in all validators if there is data and a field name #7542

Merged
merged 9 commits into from Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/migration.md
Expand Up @@ -280,14 +280,14 @@ being validated. Some of these arguments have been removed from `@field_validato
to index into `cls.model_fields`

```python
from pydantic import BaseModel, FieldValidationInfo, field_validator
from pydantic import BaseModel, ValidationInfo, field_validator


class Model(BaseModel):
x: int

@field_validator('x')
def val_x(cls, v: int, info: FieldValidationInfo) -> int:
def val_x(cls, v: int, info: ValidationInfo) -> int:
assert info.config is not None
print(info.config.get('title'))
#> Model
Expand Down
4 changes: 2 additions & 2 deletions docs/usage/serialization.md
Expand Up @@ -365,14 +365,14 @@ class DayThisYear(date):
def __get_pydantic_core_schema__(
cls, source: Type[Any], handler: GetCoreSchemaHandler
) -> core_schema.CoreSchema:
return core_schema.general_after_validator_function(
return core_schema.no_info_after_validator_function(
cls.validate,
core_schema.int_schema(),
serialization=core_schema.format_ser_schema('%Y-%m-%d'),
)

@classmethod
def validate(cls, v: int, _info):
def validate(cls, v: int):
return date.today().replace(month=1, day=1) + timedelta(days=v)


Expand Down
77 changes: 72 additions & 5 deletions docs/usage/types/custom.md
@@ -1,5 +1,3 @@


You can also define your own custom data types. There are several ways to achieve it.

## Composing types via `Annotated`
Expand Down Expand Up @@ -775,8 +773,8 @@ class MySequence(Sequence[T]):
else:
sequence_t_schema = handler.generate_schema(Sequence)

non_instance_schema = core_schema.general_after_validator_function(
lambda v, i: MySequence(v), sequence_t_schema
non_instance_schema = core_schema.no_info_after_validator_function(
MySequence, sequence_t_schema
)
return core_schema.union_schema([instance_schema, non_instance_schema])

Expand Down Expand Up @@ -807,11 +805,80 @@ except ValidationError as exc:
2 validation errors for M
s1.is-instance[MySequence]
Input should be an instance of MySequence [type=is_instance_of, input_value=['a'], input_type=list]
s1.function-after[<lambda>(), json-or-python[json=list[int],python=chain[is-instance[Sequence],function-wrap[sequence_validator()]]]].0
s1.function-after[MySequence(), json-or-python[json=list[int],python=chain[is-instance[Sequence],function-wrap[sequence_validator()]]]].0
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='a', input_type=str]
"""
```

### Access to field name

!!!note
This was not possible with Pydantic V2 to V2.3, it was [re-added](https://github.com/pydantic/pydantic/pull/7542) in Pydantic V2.4.

As of Pydantic V2.4, you can access the field name via the `handler.field_name` within `__get_pydantic_core_schema__`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize we were doing this. This ties us even more into the type != schema paradigm. Was this possible in v1? If we made the field name a runtime thing in core would this still be necessary?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, but it's the only way to make what we're doing possible.

and thereby set the field name which will be available from `info.field_name`.

```python
from typing import Any

from pydantic_core import core_schema

from pydantic import BaseModel, GetCoreSchemaHandler, ValidationInfo


class CustomType:
"""Custom type that stores the field it was used in."""

def __init__(self, value: int, field_name: str):
self.value = value
self.field_name = field_name

def __repr__(self):
return f'CustomType<{self.value} {self.field_name!r}>'

@classmethod
def validate(cls, value: int, info: ValidationInfo):
return cls(value, info.field_name)

@classmethod
def __get_pydantic_core_schema__(
cls, source_type: Any, handler: GetCoreSchemaHandler
) -> core_schema.CoreSchema:
return core_schema.with_info_after_validator_function(
cls.validate, handler(int), field_name=handler.field_name
)


class MyModel(BaseModel):
my_field: CustomType


m = MyModel(my_field=1)
print(m.my_field)
#> CustomType<1 'my_field'>
```

You can also access `field_name` from the markers used with `Annotated`, like [`AfterValidator`][pydantic.functional_validators.AfterValidator].

```python
from typing_extensions import Annotated

from pydantic import AfterValidator, BaseModel, ValidationInfo


def my_validators(value: int, info: ValidationInfo):
return f'<{value} {info.field_name!r}>'


class MyModel(BaseModel):
my_field: Annotated[int, AfterValidator(my_validators)]


m = MyModel(my_field=1)
print(m.my_field)
#> <1 'my_field'>
```

[PEP 593]: https://peps.python.org/pep-0593/
[PEP 695]: https://peps.python.org/pep-0695/
[typing-extensions]: https://github.com/python/typing_extensions
24 changes: 11 additions & 13 deletions docs/usage/validators.md
Expand Up @@ -305,8 +305,8 @@ If you want to attach a validator to a specific field of a model you can use the
```py
from pydantic import (
BaseModel,
FieldValidationInfo,
ValidationError,
ValidationInfo,
field_validator,
)

Expand All @@ -325,7 +325,7 @@ class UserModel(BaseModel):
# you can select multiple fields, or use '*' to select all fields
@field_validator('id', 'name')
@classmethod
def check_alphanumeric(cls, v: str, info: FieldValidationInfo) -> str:
def check_alphanumeric(cls, v: str, info: ValidationInfo) -> str:
if isinstance(v, str):
# info.field_name is the name of the field being validated
is_alphanumeric = v.replace(' ', '').isalnum()
Expand Down Expand Up @@ -372,7 +372,7 @@ A few things to note on validators:

* `@field_validator`s are "class methods", so the first argument value they receive is the `UserModel` class, not an instance of `UserModel`. We recommend you use the `@classmethod` decorator on them below the `@field_validator` decorator to get proper type checking.
* the second argument is the field value to validate; it can be named as you please
* the third argument, if present, is an instance of `pydantic.FieldValidationInfo`
* the third argument, if present, is an instance of `pydantic.ValidationInfo`
* validators should either return the parsed value or raise a `ValueError` or `AssertionError` (``assert`` statements may be used).
* A single validator can be applied to multiple fields by passing it multiple field names.
* A single validator can also be called on *all* fields by passing the special value `'*'`.
Expand All @@ -382,8 +382,8 @@ A few things to note on validators:
Python with the [`-O` optimization flag](https://docs.python.org/3/using/cmdline.html#cmdoption-o)
disables `assert` statements, and **validators will stop working**.

If you want to access values from another field inside a `@field_validator`, this may be possible using `FieldValidationInfo.data`, which is a dict of field name to field value.
Validation is done in the order fields are defined, so you have to be careful when using `FieldValidationInfo.data` to not access a field that has not yet been validated/populated — in the code above, for example, you would not be able to access `info.data['id']` from within `name_must_contain_space`.
If you want to access values from another field inside a `@field_validator`, this may be possible using `ValidationInfo.data`, which is a dict of field name to field value.
Validation is done in the order fields are defined, so you have to be careful when using `ValidationInfo.data` to not access a field that has not yet been validated/populated — in the code above, for example, you would not be able to access `info.data['id']` from within `name_must_contain_space`.
However, in most cases where you want to perform validation using multiple field values, it is better to use `@model_validator` which is discussed in the section below.

## Model validators
Expand Down Expand Up @@ -604,15 +604,15 @@ You can pass a context object to the validation methods which can be accessed fr
argument to decorated validator functions:

```python
from pydantic import BaseModel, FieldValidationInfo, field_validator
from pydantic import BaseModel, ValidationInfo, field_validator


class Model(BaseModel):
text: str

@field_validator('text')
@classmethod
def remove_stopwords(cls, v: str, info: FieldValidationInfo):
def remove_stopwords(cls, v: str, info: ValidationInfo):
context = info.context
if context:
stopwords = context.get('stopwords', set())
Expand All @@ -638,8 +638,8 @@ from typing import Any, Dict, List

from pydantic import (
BaseModel,
FieldValidationInfo,
ValidationError,
ValidationInfo,
field_validator,
)

Expand All @@ -660,7 +660,7 @@ class Model(BaseModel):

@field_validator('choice')
@classmethod
def validate_choice(cls, v: str, info: FieldValidationInfo):
def validate_choice(cls, v: str, info: ValidationInfo):
allowed_choices = info.context.get('allowed_choices')
if allowed_choices and v not in allowed_choices:
raise ValueError(f'choice must be one of {allowed_choices}')
Expand Down Expand Up @@ -702,7 +702,7 @@ from contextlib import contextmanager
from contextvars import ContextVar
from typing import Any, Dict, Iterator

from pydantic import BaseModel, FieldValidationInfo, field_validator
from pydantic import BaseModel, ValidationInfo, field_validator

_init_context_var = ContextVar('_init_context_var', default=None)

Expand All @@ -728,9 +728,7 @@ class Model(BaseModel):

@field_validator('my_number')
@classmethod
def multiply_with_context(
cls, value: int, info: FieldValidationInfo
) -> int:
def multiply_with_context(cls, value: int, info: ValidationInfo) -> int:
if info.context:
multiplier = info.context.get('multiplier', 1)
value = value * multiplier
Expand Down