Сериализаторы в Django REST Framework
Сериализаторы DRF на примерах: Serializer и ModelSerializer, fields и extra_kwargs, валидация, source, SerializerMethodField, вложенные данные, контекст и N+1.
Содержание · 14 разделов
- Модели для примеров
- Serializer и ModelSerializer
- Настройка полей: fields, read_only_fields, extra_kwargs
- Валидация данных
- validate_поле и validate
- source: поле из другого места
- SerializerMethodField
- Связанные объекты и вложенные сериализаторы
- Запись во вложенный сериализатор
- Контекст и request
- save(): create, update и partial
- many=True: списки объектов
- Производительность: select_related и prefetch_related
- Частые ошибки
- AssertionError: you must call .is_valid() before accessing .data
- Creating a ModelSerializer without either the ‘fields’ attribute or the ‘exclude’ attribute
- Поле не сохраняется
- KeyError: ‘request’
- Частые вопросы
- Как принимать ID связанного объекта, а отдавать его целиком?
- Как изменить формат даты в ответе?
- Как проверить уникальность сочетания полей?
- Как сделать поле необязательным или разрешить null?
- Как загрузить файл или изображение через сериализатор?
- Итог
Сериализатор (serializer) в Django REST Framework решает две задачи: превращает объекты моделей в словари, которые потом отдаются клиенту как JSON, и проверяет входящие данные перед сохранением. Короткий ответ, как это выглядит:
serializer = BookSerializer(book) # объект → dict
serializer.data
serializer = BookSerializer(data=request.data) # dict → проверка → объект
serializer.is_valid(raise_exception=True)
serializer.save()
Ниже — подробный разбор сериализаторов Django REST Framework: чем Serializer отличается от ModelSerializer, как настраивать поля, где писать валидацию, как выводить связанные данные и записывать вложенные объекты, зачем сериализатору request и как не получить сотни SQL-запросов. Примеры проверены на Django 6.1 и DRF 3.18. Как подключить DRF, написать ViewSet и роутер, — в руководстве по Django REST Framework.
Модели для примеров
from django.conf import settings
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=200)
country = models.CharField(max_length=2, blank=True)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=300)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
published = models.DateField()
rating = models.DecimalField(max_digits=3, decimal_places=1, default=0)
views = models.PositiveIntegerField(default=0)
owner = models.ForeignKey(
settings.AUTH_USER_MODEL, null=True, blank=True,
on_delete=models.SET_NULL, related_name="books",
)
def __str__(self):
return self.title
В базе есть книги «Война и мир» (Толстой, 1869, рейтинг 4.8, 15 000 просмотров), «Идиот» (Достоевский, 1869) и другие. Сериализаторы удобно класть в отдельный модуль приложения — в примерах это library/serializers.py. В проекте установлен LANGUAGE_CODE = "ru", поэтому встроенные сообщения об ошибках DRF выводятся по-русски.
Serializer и ModelSerializer
Базовый Serializer ничего не знает о моделях: поля и сохранение вы описываете сами.
from rest_framework import serializers
class BookPlainSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(max_length=300)
published = serializers.DateField()
rating = serializers.DecimalField(max_digits=3, decimal_places=1)
def create(self, validated_data):
return Book.objects.create(**validated_data)
def update(self, instance, validated_data):
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance
ModelSerializer строит поля, валидаторы и методы create()/update() по модели:
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ["id", "title", "author", "published", "rating", "views"]
read_only_fields = ["views"]
extra_kwargs = {
"title": {"min_length": 2},
"rating": {"min_value": 0, "max_value": 5},
}
Какие поля получились, видно через repr() — это лучший способ понять, что сгенерировал DRF:
>>> print(repr(BookSerializer()))
BookSerializer():
id = BigIntegerField(label='ID', read_only=True)
title = CharField(max_length=300, min_length=2)
author = PrimaryKeyRelatedField(queryset=Author.objects.all())
published = DateField()
rating = DecimalField(decimal_places=1, max_digits=3, max_value=5, min_value=0, required=False)
views = IntegerField(read_only=True)
max_length пришёл из модели, min_length и границы рейтинга — из extra_kwargs, rating стал необязательным, потому что у поля модели есть default. Внешний ключ по умолчанию — PrimaryKeyRelatedField: на выходе ID, на входе ID с проверкой существования.
| Задача | Serializer | ModelSerializer |
|---|---|---|
| Данные не из модели (форма входа, отчёт, webhook) | подходит | не подходит |
| CRUD по модели | много кода | 5–10 строк |
| Поля и валидаторы из модели | вручную | автоматически |
create() / update() | пишете сами | готовы |
Настройка полей: fields, read_only_fields, extra_kwargs
fields— явный список полей. Вариант"__all__"допустим, но в публичном API опасен: новое поле модели (например, служебное) сразу окажется в ответе. Можно задатьexclude = [...], но не одновременно сfields.read_only_fields— поля только для вывода: клиент может их прислать, но они будут проигнорированы.extra_kwargs— аргументы для автоматически созданных полей без их переобъявления:required,min_length,write_only,allow_blankи т. д. Типичный пример — пароль:{"password": {"write_only": True}}.
Если объявить поле в сериализаторе явно, extra_kwargs и read_only_fields для него уже не действуют — параметры передаются прямо в конструктор поля.
Чтение и запись на наших данных:
>>> BookSerializer(book).data
{'id': 1, 'title': 'Война и мир', 'author': 1, 'published': '1869-01-01', 'rating': '4.8', 'views': 15000}
>>> s = BookSerializer(data={"title": "Новая книга", "author": 1, "published": "2026-01-01",
... "rating": "4.0", "views": 100500})
>>> s.is_valid()
True
>>> s.validated_data
{'title': 'Новая книга', 'author': <Author: Лев Толстой>, 'published': datetime.date(2026, 1, 1), 'rating': Decimal('4.0')}
>>> s.save()
<Book: Новая книга>
>>> s.data
{'id': 7, 'title': 'Новая книга', 'author': 1, 'published': '2026-01-01', 'rating': '4.0', 'views': 0}
Обратите внимание: views=100500 молча отброшено, а в validated_data вместо ID автора уже лежит объект Author. DecimalField по умолчанию выводится строкой — так не теряется точность; если фронтенду нужны числа, задайте COERCE_DECIMAL_TO_STRING = False в настройках REST_FRAMEWORK.
Валидация данных
Проверки выполняются в is_valid() в таком порядке: сначала каждое поле (тип, max_length, min_value и т. д.), затем методы validate_<поле>(), затем общий validate(). Ошибки собираются в serializer.errors:
>>> s = BookSerializer(data={"title": "", "author": "abc", "published": "вчера", "rating": "7777"})
>>> s.is_valid()
False
>>> s.errors
{'title': [ErrorDetail(string='Это поле не может быть пустым.', code='blank')],
'author': [ErrorDetail(string='Некорректный тип. Ожидалось значение первичного ключа, получен str.', code='incorrect_type')],
'published': [ErrorDetail(string='Неправильный формат date. Используйте один из этих форматов: YYYY-MM-DD.', code='invalid')],
'rating': [ErrorDetail(string='Убедитесь, что вы ввели не более 3 цифры.', code='max_digits')]}
Во view обычно пишут serializer.is_valid(raise_exception=True) — тогда DRF сам вернёт ответ 400 с этим словарём.
validate_поле и validate
Метод validate_<имя_поля>(self, value) получает уже преобразованное значение одного поля и должен вернуть его (возможно, изменённым). Метод validate(self, attrs) видит все поля сразу — в нём проверяют связи между полями:
from datetime import date
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ["id", "title", "author", "published", "rating"]
def validate_title(self, value):
value = value.strip()
if value.lower() == "без названия":
raise serializers.ValidationError("Укажите настоящее название.")
return value
def validate(self, attrs):
published = attrs.get("published")
if published and published > date.today():
raise serializers.ValidationError({"published": "Дата публикации в будущем."})
return attrs
>>> s = BookSerializer(data={"title": " Без названия ", "author": 1, "published": "2030-01-01"})
>>> s.is_valid(), s.errors
(False, {'title': [ErrorDetail(string='Укажите настоящее название.', code='invalid')]})
>>> s = BookSerializer(data={"title": " Новая книга ", "author": 1, "published": "2030-01-01"})
>>> s.is_valid(), s.errors
(False, {'published': [ErrorDetail(string='Дата публикации в будущем.', code='invalid')]})
Во втором случае название прошло проверку (и было очищено от пробелов: validated_data["title"] == "Новая книга"), а ошибка пришла из validate(). Если в validate() передать в ValidationError строку, а не словарь, ошибка попадёт в ключ non_field_errors. Метод validate() не вызывается, если хотя бы одно поле не прошло проверку.
source: поле из другого места
Аргумент source указывает, откуда брать значение: атрибут по связи, свойство, метод без аргументов или вложенный атрибут через точку.
class BookListSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source="author.name", read_only=True)
year = serializers.IntegerField(source="published.year", read_only=True)
is_popular = serializers.SerializerMethodField()
class Meta:
model = Book
fields = ["id", "title", "author_name", "year", "is_popular"]
def get_is_popular(self, obj):
return obj.views >= 10_000
>>> BookListSerializer(Book.objects.get(title="Война и мир")).data
{'id': 1, 'title': 'Война и мир', 'author_name': 'Лев Толстой', 'year': 1869, 'is_popular': True}
>>> BookListSerializer(Book.objects.get(title="Идиот")).data
{'id': 4, 'title': 'Идиот', 'author_name': 'Фёдор Достоевский', 'year': 1869, 'is_popular': False}
Особое значение source="*" передаёт в поле весь объект — это нужно для полей, которые собирают значение из нескольких атрибутов.
SerializerMethodField
SerializerMethodField вызывает метод get_<имя_поля>(self, obj) и выводит его результат (пример выше — is_popular). Поле всегда только для чтения. Имя метода можно задать явно: serializers.SerializerMethodField(method_name="calc_popularity").
Используйте его для вычислений на Python. Если значение можно посчитать в базе (количество книг, средний рейтинг), лучше добавить его через annotate() в queryset и вывести обычным полем: метод, который делает запрос к базе, выполнится для каждого объекта списка.
Связанные объекты и вложенные сериализаторы
Для внешних ключей есть несколько готовых полей:
| Поле | Вывод | Запись |
|---|---|---|
PrimaryKeyRelatedField | 1 | ID объекта |
SlugRelatedField(slug_field="name") | 'Лев Толстой' | значение поля name |
StringRelatedField() | результат __str__ | только чтение |
| вложенный сериализатор | словарь с полями объекта | только через свой create() |
class BookRelSerializer(serializers.ModelSerializer):
author = serializers.SlugRelatedField(slug_field="name", queryset=Author.objects.all())
class Meta:
model = Book
fields = ["id", "title", "author"]
# BookRelSerializer(book).data → {'id': 1, 'title': 'Война и мир', 'author': 'Лев Толстой'}
# data={"title": "X", "author": "Нет такого"} →
# {'author': [ErrorDetail(string='Объект с name=Нет такого не существует.', code='does_not_exist')]}
Чтобы отдать связанный объект целиком, вложите другой сериализатор. Для обратной связи «один-ко-многим» добавьте many=True:
class BookShortSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ["id", "title"]
class AuthorSerializer(serializers.ModelSerializer):
books = BookShortSerializer(many=True, read_only=True)
class Meta:
model = Author
fields = ["id", "name", "books"]
# AuthorSerializer(dostoevsky).data →
# {'id': 2, 'name': 'Фёдор Достоевский',
# 'books': [{'id': 3, 'title': 'Преступление и наказание'}, {'id': 4, 'title': 'Идиот'}]}
Запись во вложенный сериализатор
Если вложенное поле не помечено read_only=True, проверка данных пройдёт, но save() упадёт. В тексте ошибки указан модуль, где объявлен сериализатор, — здесь library.serializers:
AssertionError: The `.create()` method does not support writable nested fields by default.
Write an explicit `.create()` method for serializer `library.serializers.BookNestedSerializer`, or set `read_only=True` on nested serializer fields.
DRF не угадывает, что делать с вложенными данными: создать нового автора, найти существующего или обновить его. Это решаете вы в create() и update():
class AuthorShortSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = ["id", "name", "country"]
class BookNestedSerializer(serializers.ModelSerializer):
author = AuthorShortSerializer()
class Meta:
model = Book
fields = ["id", "title", "published", "author"]
def create(self, validated_data):
author_data = validated_data.pop("author")
author, _ = Author.objects.get_or_create(
name=author_data["name"],
defaults={"country": author_data.get("country", "")},
)
return Book.objects.create(author=author, **validated_data)
def update(self, instance, validated_data):
author_data = validated_data.pop("author", None)
if author_data:
for attr, value in author_data.items():
setattr(instance.author, attr, value)
instance.author.save()
return super().update(instance, validated_data)
>>> s = BookNestedSerializer(data={
... "title": "Записки из подполья", "published": "1864-01-01",
... "author": {"name": "Фёдор Достоевский", "country": "RU"},
... })
>>> s.is_valid()
True
>>> s.save()
<Book: Записки из подполья>
>>> s.data
{'id': 7, 'title': 'Записки из подполья', 'published': '1864-01-01', 'author': {'id': 2, 'name': 'Фёдор Достоевский', 'country': 'RU'}}
Существующий автор найден через get_or_create, дубль не создан. Если запись затрагивает несколько таблиц, оберните её в transaction.atomic(). Часто проще разделить сериализаторы: на запись принимать ID автора, а на чтение отдавать вложенный объект.
Контекст и request
Сериализатор не знает о HTTP-запросе, пока его не передадут в context. Generic-представления и ViewSet делают это сами (get_serializer() добавляет request, view и format), а при ручном создании передавайте контекст явно:
serializer = BookSerializer(book, context={"request": request})
Внутри сериализатора запрос доступен как self.context["request"]:
class BookOwnSerializer(serializers.ModelSerializer):
is_mine = serializers.SerializerMethodField()
class Meta:
model = Book
fields = ["id", "title", "is_mine"]
def get_is_mine(self, obj):
request = self.context.get("request")
return bool(request and obj.owner_id == request.user.id)
Чтобы автоматически подставлять текущего пользователя при создании, есть HiddenField с CurrentUserDefault() — клиент это поле не видит и не может подменить:
class BookCreateSerializer(serializers.ModelSerializer):
owner = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Book
fields = ["id", "title", "author", "published", "owner"]
Без request в контексте is_valid() упадёт с KeyError: 'request'. Альтернатива — передать значение при сохранении: serializer.save(owner=request.user). HyperlinkedModelSerializer тоже требует request, иначе получите AssertionError: HyperlinkedIdentityField requires the request in the serializer context. Как устроены пользователи и вход в Django, — в статье про аутентификацию в Django.
save(): create, update и partial
save() вызывает create(), если сериализатор создан без объекта, и update(), если объект передан первым аргументом:
BookSerializer(data=data).save() # create()
BookSerializer(book, data=data).save() # update()
При обновлении без partial=True сериализатор требует все обязательные поля (так работает PUT). Для частичного обновления (PATCH) передайте partial=True:
>>> s = BookSerializer(book, data={"rating": "4.5"})
>>> s.is_valid(), s.errors
(False, {'title': [ErrorDetail(string='Обязательное поле.', code='required')], ...})
>>> s = BookSerializer(book, data={"rating": "4.5"}, partial=True)
>>> s.is_valid()
True
Именованные аргументы save() добавляются к validated_data: serializer.save(owner=request.user). Обращаться к serializer.data до вызова is_valid() нельзя, если сериализатор создан с data=..., а save() с невалидными данными вызывает AssertionError: You cannot call .save() on a serializer with invalid data.
many=True: списки объектов
С many=True DRF создаёт ListSerializer, который применяет ваш сериализатор к каждому элементу:
>>> BookSerializer(Book.objects.filter(author__name="Лев Толстой"), many=True).data
[{'id': 1, 'title': 'Война и мир', ...}, {'id': 2, 'title': 'Анна Каренина', ...}]
То же работает на вход: BookSerializer(data=[{...}, {...}], many=True) проверит все элементы, а save() вернёт список созданных объектов. По умолчанию каждый объект создаётся отдельным INSERT; для массовой вставки через bulk_create() переопределяют create() у собственного ListSerializer (Meta.list_serializer_class).
Производительность: select_related и prefetch_related
Сериализатор обращается к связанным объектам для каждого элемента списка. Если queryset не подготовлен, получается проблема N+1. Замер на шести книгах и четырёх авторах:
from django.db import connection
from django.test.utils import CaptureQueriesContext
with CaptureQueriesContext(connection) as ctx:
BookListSerializer(Book.objects.all(), many=True).data
print(len(ctx.captured_queries)) # 7: список книг + автор для каждой книги
with CaptureQueriesContext(connection) as ctx:
BookListSerializer(Book.objects.select_related("author"), many=True).data
print(len(ctx.captured_queries)) # 1: JOIN
Для вложенного books с many=True нужен prefetch_related: AuthorSerializer(Author.objects.all(), many=True) выполнил 5 запросов, а с Author.objects.prefetch_related("books") — 2. В ViewSet подготовку делают в атрибуте queryset или методе get_queryset(). Как находить такие запросы в реальном проекте, — в статье о проблеме N+1, а про сами условия выборки — в разборе filter() и Q-объектов.
Частые ошибки
AssertionError: you must call .is_valid() before accessing .data
Сериализатор создан с data=..., а вы читаете .data до проверки. Вызовите is_valid() или используйте serializer.initial_data, если нужны сырые входные данные.
Creating a ModelSerializer without either the ‘fields’ attribute or the ‘exclude’ attribute
В Meta нет ни fields, ни exclude. DRF требует явно указать набор полей — хотя бы fields = "__all__".
Поле не сохраняется
Поле указано в read_only_fields, объявлено с read_only=True или его нет в fields. DRF не сообщает об игнорируемых полях — проверьте repr(serializer).
KeyError: ‘request’
Используется CurrentUserDefault, HyperlinkedModelSerializer или self.context["request"], а сериализатор создан вручную без контекста. Передайте context={"request": request}.
Частые вопросы
Как принимать ID связанного объекта, а отдавать его целиком?
Оставьте поле author обычным PrimaryKeyRelatedField для записи и подмените вывод в to_representation():
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ["id", "title", "author", "published"]
def to_representation(self, instance):
data = super().to_representation(instance)
data["author"] = AuthorShortSerializer(instance.author).data
return data
# вход: {"title": "Война и мир", "author": 1, "published": "1869-01-01"}
# выход: {'id': 1, 'title': 'Война и мир', 'author': {'id': 1, 'name': 'Лев Толстой', 'country': ''}, ...}
Своего create() не нужно: вложенного поля на запись нет. Не забудьте select_related("author") в queryset.
Как изменить формат даты в ответе?
Для одного поля задайте format, а чтобы принимать дату в том же виде — input_formats:
class BookSerializer(serializers.ModelSerializer):
published = serializers.DateField(format="%d.%m.%Y", input_formats=["%d.%m.%Y", "iso-8601"])
# ...
# → {'id': 1, 'title': 'Война и мир', 'published': '01.01.1869'}
Для всего API есть настройки DATE_FORMAT, DATETIME_FORMAT и DATE_INPUT_FORMATS в словаре REST_FRAMEWORK.
Как проверить уникальность сочетания полей?
Если в модели есть UniqueConstraint по полям author и title, ModelSerializer добавит проверку сам. Без ограничения в модели подключите UniqueTogetherValidator:
from rest_framework.validators import UniqueTogetherValidator
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ["id", "title", "author", "published"]
validators = [
UniqueTogetherValidator(
queryset=Book.objects.all(),
fields=["author", "title"],
message="У этого автора уже есть книга с таким названием.",
)
]
Ошибка придёт в ключе non_field_errors. Проверка на уровне сериализатора не защищает от гонки двух одновременных запросов — для гарантии нужно ограничение в базе.
Как сделать поле необязательным или разрешить null?
У поля три независимых параметра: required=False — поле можно не передавать, allow_null=True — можно передать null, allow_blank=True — для строк допустима пустая строка.
subtitle = serializers.CharField(required=False, allow_blank=True)
pages = serializers.IntegerField(required=False, allow_null=True)
В ModelSerializer эти параметры выводятся из blank, null и default поля модели, а поменять их можно через extra_kwargs.
Как загрузить файл или изображение через сериализатор?
Для FileField и ImageField модели (например, cover = models.ImageField(upload_to="covers/")) ModelSerializer создаёт такие же поля сериализатора. Клиент отправляет запрос в формате multipart/form-data, а не JSON — парсер для него в DRF включён по умолчанию:
curl -X POST http://127.0.0.1:8000/api/books/ -H "Authorization: Bearer <access>" -F "title=Война и мир" -F "author=1" -F "published=1869-01-01" -F "cover=@cover.jpg"
В ответе поле содержит URL файла. Абсолютный адрес с доменом получится, только если в контексте сериализатора есть request — generic-представления и ViewSet передают его сами.
Итог
ModelSerializer закрывает типовой CRUD: поля настраиваются через fields, read_only_fields и extra_kwargs, проверки — в validate_<поле>() и validate(). Для вывода данных пригодятся source, SerializerMethodField и вложенные сериализаторы, а запись во вложенные объекты всегда требует своего create()/update(). Не забывайте готовить queryset через select_related/prefetch_related.
Следующий шаг — собрать из сериализаторов полноценный API с ViewSet, правами и пагинацией по руководству по Django REST Framework. Если API пишется не на Django, похожую роль в FastAPI играют модели Pydantic. Полный справочник — в документации DRF по сериализаторам.
- #Django REST Framework
- #DRF
- #Сериализаторы
- #ModelSerializer
- #Валидация