Валидация в DRF: где размещать проверки и чем платить
В сериализаторе, в валидаторе поля, в методе validate или в модели — DRF даёт четыре места под одну проверку. Разбираем, что где уместно и чем оборачивается неверный выбор.

Одну и ту же проверку в Django REST Framework можно повесить в четырёх местах: в аргументе поля через validators, в методе validate_<field>, в методе validate всего сериализатора и в clean/ограничениях модели. Все четыре внешне работают. Разница вылезает, когда проверка зависит от нескольких полей, когда ошибку нужно показать под конкретным полем, а не общим списком, и когда те же данные приходят не через API, а через админку или management-команду. Ниже — где какая проверка живёт правильно и чем оборачивается неверное размещение. Актуально для DRF 3.14–3.15 и Django 4.2–5.x.
Четыре уровня и что каждый видит
Валидатор поля и validate_<field> работают с одним значением и ничего не знают о соседних полях. Метод validate получает уже словарь провалидированных полей — здесь доступны кросс-полевые проверки. Модельные ограничения и clean срабатывают ниже, на уровне БД и ORM, независимо от того, кто пишет данные.
| Место | Что видит | Куда попадёт ошибка |
|---|---|---|
validators=[...] у поля | одно значение поля | под этим полем |
validate_<field>(self, value) | одно значение поля | под этим полем |
validate(self, attrs) | словарь всех полей | non_field_errors или под указанным полем |
модель: clean, constraints | объект и БД | ошибка ORM/IntegrityError |
Порядок вызова в сериализаторе фиксированный: сначала приведение типа поля (to_internal_value), затем валидаторы поля и validate_<field>, и только потом validate целиком. Если поле не прошло свою проверку, оно не попадёт в attrs, и в validate его там просто не будет.
Проверка одного поля: валидатор против метода
Простое ограничение на одно поле — минимальная длина, диапазон, регулярка — вешают через validators. Плюс: валидатор переиспользуется между сериализаторами, его видно в схеме OpenAPI. Минус: он не имеет доступа к self, то есть к контексту запроса.
from rest_framework import serializers
def validate_positive(value):
# Отдельная функция-валидатор — переиспользуется в разных сериализаторах
if value <= 0:
raise serializers.ValidationError("Значение должно быть больше нуля")
return value
class OrderSerializer(serializers.Serializer):
quantity = serializers.IntegerField(validators=[validate_positive])
def validate_quantity(self, value):
# Метод нужен, когда проверка зависит от контекста или self
max_qty = self.context["request"].user.limit
if value > max_qty:
raise serializers.ValidationError(
f"Больше {max_qty} за раз заказать нельзя"
)
return value
Критерий выбора: нужен ли доступ к self.context или другим данным. Нет — выносите в отдельную функцию-валидатор. Да — только validate_<field>.
Проверка нескольких полей: только validate
Сравнение двух полей — «дата окончания не раньше даты начала», «пароль совпадает с подтверждением» — нельзя корректно сделать на уровне одного поля. Валидатор quantity не знает про price. Для этого есть validate(self, attrs):
class BookingSerializer(serializers.Serializer):
start = serializers.DateField()
end = serializers.DateField()
def validate(self, attrs):
if attrs["end"] < attrs["start"]:
# Ошибку можно привязать к конкретному полю через словарь
raise serializers.ValidationError(
{"end": "Дата окончания раньше даты начала"}
)
return attrs
Здесь важна деталь: если по required=True поле не пришло, до validate дело не дойдёт — сериализатор упадёт раньше на проверке обязательности. А если поле required=False и не передано, ключа start в attrs не будет вовсе. Обращение по attrs["start"] кинет KeyError вместо аккуратной ошибки валидации. Пользуйтесь attrs.get(...) и проверяйте на None.
Правило простое: одно поле — валидатор илиvalidate_<field>; несколько полей —validate; ограничение, которое должно держаться при любой записи, — модель. Всё остальное — вопрос удобства и переиспользования.
Уникальность и проверки, которые лезут в БД
«Такой email уже занят», «этот слаг не должен повторяться в рамках проекта» — это проверки уникальности. В DRF под них есть UniqueValidator для одного поля и UniqueTogetherValidator для комбинации. Если сериализатор построен через ModelSerializer, эти валидаторы часто добавляются автоматически из unique=True и unique_together/UniqueConstraint модели.
from rest_framework.validators import UniqueTogetherValidator
class MembershipSerializer(serializers.ModelSerializer):
class Meta:
model = Membership
fields = ["user", "project", "role"]
validators = [
UniqueTogetherValidator(
queryset=Membership.objects.all(),
fields=["user", "project"],
)
]
Ключевая мысль: валидатор уникальности в сериализаторе даёт красивую ошибку под полем, но не защищает данные. Он делает SELECT перед записью, и между этим SELECT и вставкой другой запрос может создать дубль — классическая гонка. Настоящую гарантию даёт только UniqueConstraint на уровне БД. Поэтому серьёзную уникальность держат в двух местах сразу: UniqueConstraint в модели ради гарантии, валидатор в сериализаторе ради человеческого сообщения об ошибке.
Почему нельзя всё сваливать в validate с запросами к БД
Соблазн написать один большой validate с десятком .exists() и .filter() заканчивается медленным эндпоинтом. Каждый такой вызов — отдельный запрос. При обработке списка объектов через many=True это множится на количество элементов и даёт классический N+1. Если проверка требует обращения к БД по каждому элементу — либо предзагружайте нужные данные одним запросом до цикла валидации, либо переносите проверку в БД-ограничение.
Модельный уровень: когда он обязателен
Валидация в сериализаторе срабатывает только на пути через DRF. Данные, залитые через Django-админку, management-команду, сигнал или прямой Model.objects.create(), ваш validate не увидят. Если инвариант обязан держаться всегда — «сумма не может быть отрицательной», «статус только из разрешённого набора» — его место в модели: CheckConstraint, UniqueConstraint, choices, а для сложной логики — метод clean.
При этом full_clean (а значит и clean) Django сам при обычном save() не вызывает. Так что clean — не гарантия, а помощник для форм и админки. Гарантию на уровне данных даёт только constraint в БД, который переживёт любой способ записи.
Когда что выбирать
- Формат и диапазон одного поля, без контекста — функция-валидатор в
validators=[...]. Переиспользуется, видна в схеме. - Проверка одного поля с доступом к пользователю или объекту —
validate_<field>. - Сравнение или согласованность нескольких полей —
validate(self, attrs)сattrs.get. - Уникальность ради сообщения —
UniqueValidator/UniqueTogetherValidator. - Инвариант, который обязан держаться при любой записи —
CheckConstraint/UniqueConstraintв модели, плюс дублирующий валидатор для человеческого текста.
Что ломается на практике
- KeyError в validate. Обращение к необязательному полю по ключу вместо
attrs.get. Отдаёт 500 вместо 400. - Проверка не сработала на partial update. При
PATCH(partial=True) поля, которых нет в запросе, отсутствуют вattrs. Кросс-полевая логика вvalidateможет тихо пропустить проверку. - Уникальность в валидаторе, но не в БД. Дубли под нагрузкой из-за гонки. Лечится только constraint в модели и миграцией.
- Логика ушла в вычисления вместо валидации. Проверки, которые меняют данные (нормализация, приведение), лучше делать в
to_internal_valueили явных методах, а не прятать вvalidate.
Что делать дальше
Пройдитесь по своим сериализаторам и разложите каждую проверку по одному из пяти правил выше. Для всех проверок уникальности убедитесь, что под красивым валидатором есть UniqueConstraint в модели и соответствующая миграция — это единственная защита от гонок. Отдельная тема — валидация вложенных и списковых сериализаторов (many=True) и порядок ошибок в них; здесь мы её не разворачивали. Дальше стоит посмотреть раздел про валидаторы в официальной документации DRF и раздел про constraints в документации Django.
Проверка рекламы на риски: Что в макете вызовет вопросы ФАС: превосходные степени, сравнения, обещания результата, маркировка.
Спросить за 40 ₽Источники: Django REST Framework — Validators, Django — Constraints reference
Частые вопросы
У меня DRF 3.12, а не 3.14 — что-то отличается?
Почему проверка в validate не срабатывает при PATCH-запросе?
Достаточно ли UniqueValidator, чтобы не было дублей в базе?
Куда девать нормализацию данных — обрезку пробелов, приведение регистра?
Что делать, если данные пишутся ещё и через админку и команды?
Валидатор поля или validate_<field> — что выбрать, если оба подходят?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.