DRF: сериализаторы и вьюсеты без дублирования логики
Разбираем, где в Django REST Framework провести границу между сериализатором и вьюсетом, чтобы одна валидация не расползлась по трём местам, а список и деталь не тянули одинаковый тяжёлый запрос.

Типичная проблема на разросшемся API: одно и то же поле валидируется в трёх местах, список объектов отдаёт вложенные связи, которые нужны только на детальной странице, а метод create вьюсета переопределён, потому что «в сериализаторе не получилось». Всё это — следствие размытой границы между сериализатором и вьюсетом. Ниже — как эту границу провести и по каким критериям выбирать один сериализатор на все действия или несколько. Актуально для DRF 3.14+ и Django 4.2/5.x.
Кто за что отвечает
Формально роли простые, но именно на их смешении вырастают проблемы:
- Сериализатор — преобразование между Python-объектами и примитивами (dict, list, str, int) и валидация данных. Он не знает, кто прислал запрос и какой это HTTP-метод.
- Вьюсет — маршрутизация действий (
list,retrieve,create,update,destroy), выбор queryset, права доступа, пагинация, фильтрация. Он решает что обрабатывать и кому отдавать.
Практическое правило: если логика зависит от того, как выглядят данные объекта, — это сериализатор. Если зависит от того, кто и каким методом обращается, — это вьюсет.
Валидацию, которая касается только полей и их сочетаний, всегда держите в сериализаторе. Как только вы лезете за request.user или проверяете HTTP-метод внутри сериализатора — это сигнал, что кусок логики уехал не туда.
Сериализатор: где живёт валидация
В DRF три уровня валидации, и путать их — источник дублей.
- Валидация поля — метод
validate_<имя_поля>. Проверяет одно поле изолированно. - Валидация объекта — метод
validate(self, attrs). Проверяет поля в связке: например, что дата окончания не раньше даты начала. - Валидаторы уровня поля/сериализатора — переиспользуемые классы или функции в
validators. Годятся, когда одну и ту же проверку нужно применить в нескольких сериализаторах.
Пример корректного распределения:
# DRF 3.14, Django 5.x
from rest_framework import serializers
from .models import Booking
class BookingSerializer(serializers.ModelSerializer):
class Meta:
model = Booking
fields = ["id", "room", "start", "end", "guest_name"]
# валидация одного поля
def validate_guest_name(self, value):
if len(value.strip()) < 2:
raise serializers.ValidationError("Имя гостя слишком короткое")
return value
# валидация связки полей
def validate(self, attrs):
# при частичном обновлении поля может не быть в attrs
start = attrs.get("start", getattr(self.instance, "start", None))
end = attrs.get("end", getattr(self.instance, "end", None))
if start and end and end <= start:
raise serializers.ValidationError(
{"end": "Дата окончания должна быть позже даты начала"}
)
return attrs
Обратите внимание на attrs.get(..., getattr(self.instance, ...)): при PATCH (частичное обновление) в attrs попадают только присланные поля, и без обращения к self.instance валидация связки развалится. Это одна из самых частых ошибок.
Вьюсет: где живёт всё остальное
ModelViewSet закрывает пять действий одним классом. Переопределять его методы приходится реже, чем кажется. Разберём частые ситуации и куда их класть.
Разный queryset для разных пользователей
Это точно вьюсет — зависит от request.user:
from rest_framework import viewsets
from .models import Booking
from .serializers import BookingSerializer
class BookingViewSet(viewsets.ModelViewSet):
serializer_class = BookingSerializer
def get_queryset(self):
user = self.request.user
qs = Booking.objects.all()
if not user.is_staff:
qs = qs.filter(owner=user)
return qs
Подставить владельца при создании
Владелец не приходит из тела запроса — его знает вьюсет. Правильное место — perform_create, а не переопределение create целиком:
def perform_create(self, serializer):
# owner берём из запроса, а не доверяем клиенту
serializer.save(owner=self.request.user)
Полное переопределение create нужно, только когда вы меняете структуру ответа или статус-код. Для «подставить пару полей» хватает perform_create / perform_update.
Один сериализатор или несколько
Главный вопрос, из-за которого API либо остаётся читаемым, либо превращается в набор флагов внутри одного класса. Есть три рабочих подхода.
| Подход | Что выигрываете | Чем платите |
|---|---|---|
| Один сериализатор на всё | Меньше кода, одно место правды по полям | Список тянет тяжёлые вложенные связи; поля «только на запись» и «только на чтение» смешиваются |
Разные сериализаторы через get_serializer_class |
Лёгкий список, подробная деталь, отдельная форма записи | Больше классов; логику валидации легко продублировать, если не вынести в базовый |
Один класс + read_only/write_only поля |
Разделение чтения и записи без размножения классов | Не решает проблему тяжёлого списка — вложенности всё равно грузятся |
Выбор сериализатора по действию делается во вьюсете:
def get_serializer_class(self):
if self.action == "list":
return BookingListSerializer # только id, room, start, end
return BookingDetailSerializer # + вложенный гость, история
Когда что выбирать
- Один класс — пока сущность плоская и в списке отдаётся то же, что в детали. Не усложняйте раньше времени.
- Разные классы по действию — как только список должен быть легче детали или форма создания принимает поля, которых нет в ответе. Порог — появление первого вложенного сериализатора, который не нужен в списке.
- read_only/write_only — когда набор полей один, но часть принимается только на запись (например, пароль) и не должна возвращаться.
Чтобы разные сериализаторы не дублировали валидацию, выносите общие проверки в базовый класс и наследуйтесь от него. Метод validate из базового наследуется как обычный Python-метод.
Что ломается на практике
- N+1 запросов в списке. Вложенный сериализатор по связи без
select_related/prefetch_relatedв queryset даёт отдельный SQL-запрос на каждый объект. Лечится вget_querysetвьюсета, а не в сериализаторе. Ловите черезdjango-debug-toolbarили логирование SQL. - Валидация связки полей падает на PATCH. Причина — обращение к
attrs['field']вместоattrs.get(...)с фолбэком наself.instance. Проявляется какKeyErrorпри частичном обновлении. - Логика прав внутри сериализатора. Проверки вида «если пользователь не админ — запретить поле» через
self.context['request']работают, но размывают ответственность. Права — этоpermission_classesиget_querysetвьюсета. - Переопределённый
createвместоperform_create. Полное переопределение теряет обработку ошибок и стандартный ответ. Меняете только то, что реально нужно. - Поле
write_onlyзабыли пометить. Пароль или токен уезжает в ответ. Проверяется просмотром реального JSON ответа, а не кода.
Что делать дальше
Проверьте текущий API по трём точкам. Первое — включите логирование SQL и откройте список: если число запросов растёт с числом объектов, добавьте select_related/prefetch_related в get_queryset. Второе — пройдитесь по сериализаторам и найдите обращения к context['request']: каждое — кандидат на переезд во вьюсет. Третье — проверьте, что PATCH с одним полем не падает.
Дальше стоит посмотреть в сторону drf-spectacular для генерации OpenAPI-схемы (разные сериализаторы по действию она подхватывает), а также разобраться с вложенной записью через связи — это отдельная большая тема, здесь мы её не разворачивали. Синтаксис методов и хуков сверяйте по официальной документации DRF для вашей версии.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Django REST Framework — Serializers, Django REST Framework — ViewSets
Частые вопросы
У меня DRF 3.12, а не 3.14 — примеры подойдут?
Валидация в validate() не срабатывает при PATCH — почему?
Стоит ли всегда делать отдельный сериализатор для списка?
Где ставить проверку прав доступа — в сериализаторе или вьюсете?
Как поймать N+1 запросы, если список отдаётся медленно?
Можно ли переопределять create() целиком?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.