Skip to content

Commit

Permalink
Allow access to field_name and data in all validators if there is…
Browse files Browse the repository at this point in the history
… data and a field name (#7542)

Co-authored-by: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>
  • Loading branch information
samuelcolvin and adriangb committed Sep 22, 2023
1 parent ff87673 commit dd2826e
Show file tree
Hide file tree
Showing 29 changed files with 527 additions and 267 deletions.
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__`
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

0 comments on commit dd2826e

Please sign in to comment.