2025, Oct 18 07:00

Attach password mismatch errors to the confirmation field in Pydantic using an after validator

Learn how to do cross-field validation in Pydantic and attach password mismatch errors to the confirmation field using an after validator and info.data.

Cross-field validation is a common requirement: two inputs are individually valid, yet the combination is not. A classic case is password confirmation. Field-level checks for length and presence work fine, but when the values don’t match, the error often ends up detached from any specific field. The goal is to attach the error to the confirmation field so the client knows exactly what to fix.

Reproducing the issue

The following model validates length and emptiness for both inputs and then compares them in a model-level check. The logic is sound, but the mismatch error is not bound to a particular field.

from pydantic import BaseModel, Field, field_validator, model_validator


class CredentialInput(BaseModel):
    pwd: str = Field(..., min_length=8, max_length=128)
    pwd_repeat: str = Field(..., min_length=8, max_length=128)

    @field_validator("pwd")
    def ensure_pwd_present(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("Password required")
        return v

    @field_validator("pwd_repeat")
    def ensure_pwd_repeat_present(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("Confirm Password Required")
        return v

    @model_validator(mode="before")
    def verify_pair_equality(cls, vals):
        p = vals.get("pwd")
        r = vals.get("pwd_repeat")
        if p != r:
            raise ValueError("Passwords does not match")
        return vals

What’s happening and why

The equality check runs at the model level in a “before” validator. That means the error is raised for the entire model rather than a specific field, so it won’t be attached to an individual path like the confirmation input. Field-level validators, on the other hand, report errors on their associated field. To make the mismatch appear on the confirmation input, the comparison must be performed as a field-level validation on that input.

The fix: use an after field validator with access to peer data

After-validators can access already parsed fields via the info argument. By validating the confirmation field in mode="after" and reading the primary password from info.data, the mismatch error is naturally bound to the confirmation field.

from pydantic import BaseModel, Field, field_validator
from pydantic_core.core_schema import ValidationInfo


class CredentialInput(BaseModel):
    pwd: str = Field(..., min_length=8, max_length=128)
    pwd_repeat: str = Field(..., min_length=8, max_length=128)

    @field_validator("pwd")
    @classmethod
    def ensure_pwd_present(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("Password required")
        return v

    @field_validator("pwd_repeat")
    @classmethod
    def ensure_pwd_repeat_present(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("Confirm Password Required")
        return v

    @field_validator("pwd_repeat", mode="after")
    @classmethod
    def ensure_match(cls, value: str, info: ValidationInfo) -> str:
        if value != info.data["pwd"]:
            raise ValueError("Passwords do not match")
        return value

This approach mirrors the documented pattern: after-validators run once the target field is parsed and let you read sibling fields from info.data, which makes it possible to tie cross-field errors to a specific input.

Why this matters

Error placement is not just cosmetic. APIs and frontends rely on predictable error paths to highlight the exact input that needs attention. A general model error forces clients to guess which field to fix, while a field-scoped error provides a direct and actionable message. In password confirmation flows, attaching the mismatch to the confirmation field is the expected and user-friendly behavior.

Notes on broader use

The same technique applies when multiple comparisons are required in one request. After-validators can read parsed data through info.data. It’s worth noting that, when there are multiple after validators, it’s possible for one to see all fields in info.data while another finds it empty. Keep this in mind when arranging validation steps.

Conclusion

When you need a cross-field check to land on a specific field, move the comparison into a field-level validator in mode="after" and access the other field via info.data. This keeps the error anchored to the intended input, improves client-side UX, and aligns with the documented validation flow.

The article is based on a question from StackOverflow by Mr. Kenneth and an answer by ken.