Search code examples
pythonpydantic

Pydantic V2 - @field_validator `values` argument equivalent


I'm migrating from v1 to v2 of Pydantic and I'm attempting to replace all uses of the deprecated @validator with @field_validator.

Previously, I was using the values argument to my validator function to reference the values of other previously validated fields. As the v1 docs say:

You can also add any subset of the following arguments to the signature (the names must match):

  • values: a dict containing the name-to-value mapping of any previously-validated fields

It seems this values argument is no longer passed as the @field_validator signature has changed. However, the migration docs don't mention a values equivalent in v2 and the v2 validator documentation page has not yet been updated for v2.0.

Does anyone know the preferred approach for v2?

V1 validator:

@validator('password2')
def passwords_match(cls, v, values, **kwargs):
    if 'password1' in values and v != values['password1']:
        raise ValueError('passwords do not match')
    return v

Solution

  • The current version of the Pydantic v2 documentation is actually up to date for the field validators section in terms of what the signature of your validation method must/can look like.

    • the second argument is the field value to validate; it can be named as you please
    • the third argument is an instance of pydantic.ValidationInfo

    [...]

    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 [...]

    (Side note: You can look up a bit more information about the ValidationData protocol in the Pydantic Core API reference, although it is a bit terse and could do with some cross-references.)


    The old way (v1)

    Say you had the following code in Pydantic v1:

    from typing import Any
    
    from pydantic import BaseModel, ValidationError, validator
    
    
    class UserModel(BaseModel):
        ...
        password1: str
        password2: str
    
        @validator("password2")
        def passwords_match(cls, v: str, values: dict[str, Any]) -> str:
            if "password1" in values and v != values["password1"]:
                raise ValueError("passwords do not match")
            return v
    
    
    try:
        UserModel(password1="abc", password2="xyz")
    except ValidationError as err:
        print(err.json(indent=4))
    

    Output:

    [
        {
            "loc": [
                "password2"
            ],
            "msg": "passwords do not match",
            "type": "value_error"
        }
    ]
    

    The new way (v2) using @field_validator

    You would have to rewrite the v1 code above like this in v2:

    from pydantic import BaseModel, ValidationError, ValidationInfo, field_validator
    
    
    class UserModel(BaseModel):
        ...
        password1: str
        password2: str
    
        @field_validator("password2")
        def passwords_match(cls, v: str, info: ValidationInfo) -> str:
            if "password1" in info.data and v != info.data["password1"]:
                raise ValueError("passwords do not match")
            return v
    
    
    try:
        UserModel(password1="abc", password2="xyz")
    except ValidationError as err:
        print(err.json(indent=4))
    

    v2 output:

    [
        {
            "type": "value_error",
            "loc": [
                "password2"
            ],
            "msg": "Value error, passwords do not match",
            "input": "xyz",
            "ctx": {
                "error": "passwords do not match"
            },
            "url": "https://errors.pydantic.dev/2.6/v/value_error"
        }
    ]
    

    Another way (v2) using an annotated validator

    For the sake of completeness, Pydantic v2 offers a new way of validating fields, which is annotated validators. The code above could just as easily be written with an AfterValidator (for example) like this:

    from typing import Annotated
    
    from pydantic import AfterValidator, BaseModel, ValidationError, ValidationInfo
    
    
    def ensure_passwords_match(v: str, info: ValidationInfo) -> str:
        if "password1" in info.data and v != info.data["password1"]:
            raise ValueError("passwords do not match")
        return v
    
    
    class UserModel(BaseModel):
        ...
        password1: str
        password2: Annotated[str, AfterValidator(ensure_passwords_match)]
    
    
    try:
        UserModel(password1="abc", password2="xyz")
    except ValidationError as err:
        print(err.json(indent=4))
    

    The output is exactly the same as in the @field_validator example.

    Background

    As you can see from the Pydantic core API docs linked above, annotated validator constructors take the same type of argument as the decorator returned by @field_validator, namely either a NoInfoValidatorFunction or a WithInfoValidatorFunction, so either a Callable[[Any], Any] or a Callable[[Any, ValidationInfo], Any].

    (Strictly speaking the signature of the field_validator inner decorator is different because it technically can deal with more nuances like implicit classmethods etc., but it is designed to essentially deal with the same type of functions.)

    Key takeaway

    Field-specific validator functions should therefore always have either

    • one parameter - the value to validate, or
    • two parameters - the value and the ValidationInfo object.