2025, Oct 18 07:16

Как привязать несоответствие паролей к полю в Pydantic

Разбираем, как в Pydantic корректно проверять подтверждение пароля: перенос сравнения в after-валидатор поля, доступ к info.data и отказ от model_validator.

Проверка взаимосвязанных полей — частая задача: каждое значение по отдельности корректно, но их сочетание — нет. Типичный пример — подтверждение пароля. Проверки на длину и непустоту на уровне полей работают как надо, однако при несовпадении значений ошибка нередко «отвязывается» от конкретного поля. Наша цель — привязать её к полю подтверждения, чтобы клиент точно понимал, что именно нужно исправить.

Как воспроизвести проблему

В приведённой ниже модели оба поля проверяются на длину и непустоту, после чего значения сравниваются на уровне всей модели. Логика корректная, но ошибка несоответствия не привязана к конкретному полю.

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

Что происходит и почему

Проверка на равенство выполняется на уровне модели в валидаторе с режимом “before”. Это означает, что ошибка поднимается для всей модели, а не для отдельного поля, поэтому она не будет привязана к конкретному пути — например, к полю подтверждения. Валидаторы уровня поля, напротив, сообщают об ошибках именно для своего поля. Чтобы несоответствие появлялось на поле подтверждения, сравнение нужно выполнять как проверку на уровне этого поля.

Решение: after-валидатор поля с доступом к соседним данным

After-валидаторы получают доступ к уже разобранным значениям через аргумент info. Если проверять поле подтверждения в режиме "after" и читать исходный пароль из info.data, ошибка несоответствия автоматически привяжется именно к полю подтверждения.

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

Подход соответствует документации: after-валидаторы запускаются после разбора целевого поля и позволяют читать соседние поля из info.data, что даёт возможность «прикреплять» межполевые ошибки к конкретному полю ввода.

Почему это важно

Расположение ошибки — не вопрос косметики. API и фронтенды опираются на предсказуемые пути ошибок, чтобы подсветить именно то поле, которое требует исправления. Общая ошибка модели заставляет клиента гадать, что менять, тогда как ошибка, привязанная к полю, даёт прямую и полезную подсказку. В сценариях подтверждения пароля ожидаемое и удобное для пользователя поведение — связывать несоответствие с полем подтверждения.

Замечания о более широком применении

Тот же приём подходит и для запросов, где нужно выполнить несколько сравнений. After-валидаторы читают разобранные данные через info.data. Важно помнить: если after-валидаторов несколько, может оказаться, что один увидит в info.data все поля, а другой — пустоту. Учитывайте это при выстраивании шагов валидации.

Итог

Если межполевая проверка должна «ложиться» на конкретное поле, перенесите сравнение в валидатор уровня поля с mode="after" и получите доступ к соседнему значению через info.data. Так ошибка остаётся привязанной к нужному полю ввода, улучшается UX на стороне клиента и это соответствует описанному в документации процессу валидации.

Материал основан на вопросе на StackOverflow от Mr. Kenneth и ответе ken.