Django-Admin.ru

Сериализаторы в Django REST Framework

Сериализаторы DRF на примерах: Serializer и ModelSerializer, fields и extra_kwargs, валидация, source, SerializerMethodField, вложенные данные, контекст и N+1.

Редакция Django-Admin.ru
· обновлено · 14 мин чтения

Содержание · 14 разделов
  1. Модели для примеров
  2. Serializer и ModelSerializer
  3. Настройка полей: fields, read_only_fields, extra_kwargs
  4. Валидация данных
  5. validate_поле и validate
  6. source: поле из другого места
  7. SerializerMethodField
  8. Связанные объекты и вложенные сериализаторы
  9. Запись во вложенный сериализатор
  10. Контекст и request
  11. save(): create, update и partial
  12. many=True: списки объектов
  13. Производительность: select_related и prefetch_related
  14. Частые ошибки
  15. AssertionError: you must call .is_valid() before accessing .data
  16. Creating a ModelSerializer without either the ‘fields’ attribute or the ‘exclude’ attribute
  17. Поле не сохраняется
  18. KeyError: ‘request’
  19. Частые вопросы
  20. Как принимать ID связанного объекта, а отдавать его целиком?
  21. Как изменить формат даты в ответе?
  22. Как проверить уникальность сочетания полей?
  23. Как сделать поле необязательным или разрешить null?
  24. Как загрузить файл или изображение через сериализатор?
  25. Итог

Сериализатор (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 с проверкой существования.

ЗадачаSerializerModelSerializer
Данные не из модели (форма входа, отчёт, 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 и вывести обычным полем: метод, который делает запрос к базе, выполнится для каждого объекта списка.

Связанные объекты и вложенные сериализаторы

Для внешних ключей есть несколько готовых полей:

ПолеВыводЗапись
PrimaryKeyRelatedField1ID объекта
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).

Сериализатор обращается к связанным объектам для каждого элемента списка. Если 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
  • #Валидация
↑ ↓ выборEnter открытьEsc закрыть