Serializer, ModelSerializer, nested serializers, validation
Статус: актуально для Django 5.2/6.0 и современных версий Django REST Framework.
Сериализатор задаёт контракт API: какие данные приложение принимает, как их проверяет и какие поля возвращает. При этом он не «превращает модель в JSON» сам по себе. Свойство serializer.data содержит обычные Python-примитивы — словари, списки, строки и числа. В JSON их преобразует renderer DRF перед отправкой ответа.
Это различие полезно на практике: один сериализатор можно использовать с JSON, HTML-формой или другим renderer. Авторизация, транзакции и сложные правила предметной области при этом не должны незаметно растворяться в слое представления данных.
При чтении сериализатор получает объект и строит представление:
serializer = BookSerializer(book)
payload = serializer.dataПри записи он проходит другой путь:
serializer = BookSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
book = serializer.save()После успешной проверки вход доступен в validated_data, а после save() — созданный или изменённый объект в instance. Не обращайтесь к .data до .save(): после построения выходного представления DRF уже не разрешит сохранить этот сериализатор.
Serializer и ModelSerializerОбычный Serializer похож на Django Form: поля и сохранение задаются явно. Это удобно для команд, которые не совпадают один к одному с моделью.
from rest_framework import serializers
from library.models import Book
class BookSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(max_length=200)
price = serializers.DecimalField(max_digits=10, decimal_places=2)
def validate_price(self, value):
if value < 0:
raise serializers.ValidationError("Цена не может быть отрицательной.")
return value
def create(self, validated_data):
return Book.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.title = validated_data.get("title", instance.title)
instance.price = validated_data.get("price", instance.price)
instance.save(update_fields=["title", "price"])
return instanceModelSerializer выводит типы полей и часть валидаторов из модели, но контракт API всё равно следует перечислять явно.
from rest_framework import serializers
from blog.models import Post
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ["id", "title", "slug", "body", "created_at"]
read_only_fields = ["id", "slug", "created_at"]Не используйте fields = "__all__" в публичном API. После добавления в модель служебного или персонального поля оно может случайно попасть в ответ или стать доступным для записи. Явный список делает изменение контракта осознанным.
read_only=True исключает поле из входных данных, а write_only=True — из ответа. Это поведение сериализации, а не система прав доступа. Пользователь всё равно должен пройти permission-проверки, а связанный объект — принадлежать допустимому проекту или арендатору.
Типичный пример: автора поста нельзя принимать от клиента. View уже знает текущего пользователя:
class PostWriteSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ["id", "title", "body", "category"]
read_only_fields = ["id"]def perform_create(self, serializer):
serializer.save(author=self.request.user)Так клиент не сможет подставить чужой author_id. Но одной этой меры недостаточно: доступ к категории и самому объекту также нужно ограничить на уровне queryset и permissions.
Метод validate_<field>() проверяет одно поле, а validate() — сочетание нескольких. При PATCH часть полей может отсутствовать, поэтому сравнивать только значения из attrs неверно: нужно учитывать текущее состояние объекта.
class EventSerializer(serializers.ModelSerializer):
class Meta:
model = Event
fields = ["id", "name", "starts_at", "ends_at"]
read_only_fields = ["id"]
def validate(self, attrs):
starts_at = attrs.get(
"starts_at",
getattr(self.instance, "starts_at", None),
)
ends_at = attrs.get(
"ends_at",
getattr(self.instance, "ends_at", None),
)
if starts_at and ends_at and starts_at >= ends_at:
raise serializers.ValidationError(
{"ends_at": "Окончание должно быть позже начала."}
)
return attrsПроверка уникальности в сериализаторе даёт понятную ошибку, но не защищает от гонки двух запросов. Инвариант должен поддерживать UniqueConstraint в базе данных, а приложение — корректно обрабатывать IntegrityError на границе транзакции.
Сложные бизнес-операции лучше передавать сервисной функции. Сериализатор отвечает за форму команды, сервис — за права на связанные объекты, блокировки, транзакцию и побочные эффекты.
PrimaryKeyRelatedField удобен для записи внешних ключей и many-to-many связей. Опасный вариант — безусловный queryset=Category.objects.all() в многопользовательском приложении: валидным станет идентификатор категории чужой организации.
Queryset поля можно сузить при инициализации:
class PostWriteSerializer(serializers.ModelSerializer):
category = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.none(),
)
tags = serializers.PrimaryKeyRelatedField(
many=True,
queryset=Tag.objects.none(),
required=False,
)
class Meta:
model = Post
fields = ["title", "body", "category", "tags"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
tenant = self.context["tenant"]
self.fields["category"].queryset = Category.objects.filter(tenant=tenant)
self.fields["tags"].queryset = Tag.objects.filter(tenant=tenant)Контекст должен задаваться сервером, а не данными клиента. Generic views DRF автоматически добавляют в context request, format и view; собственные значения добавляют через get_serializer_context().
StringRelatedField зависит от __str__() модели. Это удобно для внутреннего интерфейса, но нестабильно как публичный контракт: изменение человекочитаемой строки незаметно изменит API. Для внешнего API надёжнее явные поля или отдельный компактный сериализатор.
HyperlinkedModelSerializer и hyperlink-поля обычно требуют request в context, чтобы построить абсолютные URL.
Вложенный сериализатор хорошо подходит для чтения:
class PublicAuthorSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "display_name"]
class PostReadSerializer(serializers.ModelSerializer):
author = PublicAuthorSerializer(read_only=True)
class Meta:
model = Post
fields = ["id", "title", "body", "author", "created_at"]Не включайте email в публичный профиль по привычке. Для кабинета пользователя создайте отдельный приватный сериализатор.
Записываемые вложенные структуры DRF не сохраняет автоматически. Нужно определить семантику: заменить весь список, дополнить его, обновить элементы по id или удалить отсутствующие. Такая операция обычно требует transaction.atomic() и проверки прав для каждого вложенного объекта. Если клиенту достаточно передать существующие идентификаторы, PrimaryKeyRelatedField(many=True) проще и прозрачнее.
SerializerMethodField вызывается для каждого объекта. Запрос вида obj.comments.count() внутри этого метода создаёт N+1 запрос. Сериализатор не должен тайно исправлять эту проблему: view обязана подготовить queryset.
class PostListSerializer(serializers.ModelSerializer):
comment_count = serializers.IntegerField(read_only=True)
is_liked = serializers.BooleanField(read_only=True)
class Meta:
model = Post
fields = ["id", "title", "comment_count", "is_liked"]Поля comment_count и is_liked здесь ожидают аннотации Count и Exists из queryset. Связи для вложенного чтения загружают через select_related() и prefetch_related(). Проверяйте число запросов отдельным тестом списка, а не только время ответа на маленькой базе.
Пароль нельзя сохранять обычным присваиванием: оно запишет исходную строку. Используйте стандартную политику Django и менеджер пользователя.
from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import validate_password
from rest_framework import serializers
User = get_user_model()
class RegistrationSerializer(serializers.ModelSerializer):
password = serializers.CharField(
write_only=True,
trim_whitespace=False,
style={"input_type": "password"},
)
class Meta:
model = User
fields = [User.USERNAME_FIELD, "password"]
def validate(self, attrs):
user = User(**{User.USERNAME_FIELD: attrs[User.USERNAME_FIELD]})
validate_password(attrs["password"], user=user)
return attrs
def create(self, validated_data):
return User.objects.create_user(**validated_data)Ограничение уникальности логина или email закрепляют в модели и базе. Наличие endpoint регистрации, подтверждение адреса, защита от перебора и политика раскрытия ошибок — отдельные продуктовые решения.
FileField и ImageField проверяют форму входа, но не делают загруженный файл безопасным. Ограничивайте размер, проверяйте фактический формат, генерируйте имя на сервере, храните пользовательские файлы отдельно от исполняемого кода и при необходимости сканируйте их асинхронно.
many=True выбирает ListSerializer и умеет сериализовать список. Это не означает безопасный bulk create или bulk update. Для массовой записи нужно отдельно определить атомарность, соответствие элементов объектам, лимит размера запроса и формат частичных ошибок.
fields = "__all__" не удалено, однако для внешнего API считается хрупкой практикой.Главный практический критерий хорошего сериализатора: его поля образуют явный и стабильный контракт, а стоимость запросов, права доступа и транзакционные правила остаются видимыми в соответствующих слоях приложения.
Далее: DRF: Views & ViewSets