Migrate @validator to @field_validator in Pydantic v2

Key takeaways:

  • Rename @validator to @field_validator and add @classmethod beneath it.
  • pre=True becomes mode="before"; the default is mode="after".
  • Reading other fields moves from the values dict to a model_validator.
  • Multiple field names still pass positionally to field_validator.
  • Verify with the contract suite that behavior is unchanged.

This is a focused slice of the Pydantic V2 Migration Guide, and it builds on Custom Validators and Field Constraints.

The Problem This Solves

The validator decorator is one of the most-used Pydantic features, so migrating it correctly is most of the work in a v1-to-v2 move. The renames are simple, but two semantic changes — the classmethod requirement and the loss of the values dict — trip people up.

Prerequisites

Step-by-Step Implementation

1. Single-field validator

# Pydantic v1
from pydantic import BaseModel, validator


class V1(BaseModel):
    name: str

    @validator("name")
    def strip(cls, v):
        return v.strip()
# Pydantic v2
from pydantic import BaseModel, field_validator


class V2(BaseModel):
    name: str

    @field_validator("name")
    @classmethod                      # Required in v2.
    def strip(cls, v: str) -> str:
        return v.strip()

2. pre=Truemode="before"

@field_validator("amount", mode="before")   # was @validator("amount", pre=True)
@classmethod
def coerce(cls, v: object) -> object:
    return int(v) if isinstance(v, str) else v

3. Cross-field logic → model_validator

from pydantic import model_validator


class DateRange(BaseModel):
    start: int
    end: int

    @model_validator(mode="after")    # Replaces a v1 validator reading `values`.
    def check_order(self) -> "DateRange":
        if self.end <= self.start:
            raise ValueError("end must be after start")
        return self

Edge Cases and Gotchas

  • Missing @classmethod. The most common error; it raises at import.
  • always=True. Default validation changed; use validate_default=True where you relied on it.
  • Reusable validators. Consider moving recurring rules to Annotated types, per Creating Reusable Custom Validators.

Verification

import pytest
from pydantic import ValidationError


def test_behaviour_unchanged():
    assert V2(name="  ada  ").name == "ada"
    with pytest.raises(ValidationError):
        DateRange(start=5, end=1)