Field, ограничения и кастомные валидаторы
Field задаёт ограничения и метаданные отдельного поля, а декораторы field_validator и model_validator добавляют собственную логику проверки — по одному полю или по модели целиком.
Базовых типов мало: нужно ограничивать длину, диапазон, формат и проверять взаимосвязи полей. Для этого есть
Fieldи валидаторы, и в v2 они называютсяfield_validator/model_validator.
Тип str говорит «строка», но не «строка от 3 до 50 символов, без пробелов по краям». Тип int не скажет «положительное число до тысячи». Эти ограничения задаются через Field. А когда правило сложнее, чем диапазон — например, «пароль и его подтверждение должны совпадать», — нужны кастомные валидаторы.
from pydantic import BaseModel, Field, field_validator, model_validator
class Registration(BaseModel):
username: str = Field(min_length=3, max_length=20)
age: int = Field(ge=0, le=120)
password: str = Field(min_length=8)
password_repeat: str
@field_validator("username")
@classmethod
def no_spaces(cls, v: str) -> str:
if " " in v:
raise ValueError("в username не должно быть пробелов")
return v
@model_validator(mode="after")
def passwords_match(self):
if self.password != self.password_repeat:
raise ValueError("пароли не совпадают")
return self
Различие важно: field_validator работает с одним полем и не видит остальные; model_validator(mode="after") запускается после проверки всех полей и видит модель целиком, поэтому именно он умеет сверять поля между собой. Любая ValueError внутри валидатора превращается FastAPI в аккуратную ошибку 422.
Как работает под капотом
Порядок таков: сперва конвертация и базовые ограничения Field, затем валидаторы полей, затем валидатор модели. Смоделируем эту цепочку на stdlib:
def field_check(name, value):
if name == "username":
if not (3 <= len(value) <= 20):
raise ValueError("username: длина 3..20")
if " " in value:
raise ValueError("username: без пробелов")
if name == "age":
if not (0 <= value <= 120):
raise ValueError("age: диапазон 0..120")
return value
def validate_registration(data):
errors = []
clean = {}
for name in ("username", "age", "password", "password_repeat"):
try:
clean[name] = field_check(name, data[name]) # ограничения + field_validator
except ValueError as e:
errors.append(str(e))
# model_validator: сверяем поля между собой
if not errors and clean["password"] != clean["password_repeat"]:
errors.append("пароли не совпадают")
return (422, errors) if errors else (200, "ok")
print(validate_registration({"username": "an na", "age": 30, "password": "12345678", "password_repeat": "12345678"}))
print(validate_registration({"username": "anna", "age": 200, "password": "abcdefgh", "password_repeat": "xxxxxxxx"}))
print(validate_registration({"username": "anna", "age": 30, "password": "abcdefgh", "password_repeat": "abcdefgh"}))
Попробуй сам ▶ Обрати внимание: проверка совпадения паролей возможна только после проверки отдельных полей — как и model_validator(mode="after").
Частые ошибки
Первая — пытаться сверять два поля внутри field_validator: он видит только своё поле. Для межполевых проверок нужен model_validator. Вторая — забывать @classmethod у field_validator и неправильную сигнатуру (в v2 это (cls, v)). Третья — использовать устаревшие имена v1 (@validator, @root_validator) в проекте на v2. Четвёртая — возвращать из валидатора None вместо проверенного значения, обнуляя поле.
Best practices
- Простые ограничения (длина, диапазон, паттерн) задавайте через
Field, а не валидаторами. - Один-полевые проверки —
field_validator; межполевые —model_validator(mode="after"). - Бросайте
ValueErrorс понятным текстом — FastAPI превратит его в 422. - Всегда возвращайте проверенное значение (или
selfв model_validator).
Режимы валидаторов: before и after
У валидаторов есть важный параметр режима. model_validator(mode="before") срабатывает до конвертации и базовой валидации — он получает сырые входные данные и удобен, когда нужно их предобработать или собрать недостающие поля. mode="after" срабатывает после, получает уже провалидированную модель и подходит для проверки взаимосвязей. То же различие есть у field_validator. Понимание момента запуска решает реальные задачи: например, если вы хотите принять дату и в виде строки, и в виде числа, нормализацию делают в before-валидаторе, до того как Pydantic применит строгую проверку типа. А вот сверку «дата окончания не раньше даты начала» — в after, когда оба поля уже корректны. Выбор режима — это выбор «до или после того, как Pydantic привёл данные в порядок», и он определяет, с чем именно работает ваш код.
Итог: Field ограничивает поле декларативно, field_validator добавляет логику одного поля, model_validator — проверки по всей модели. Порядок: ограничения → поля → модель.