Сериализаторы: преобразование и валидация

Разбираем, как сериализатор превращает объекты Django в JSON и обратно, и где в этом потоке живёт валидация.

Сериализатор — это слой DRF, который в одну сторону преобразует объекты Python (например, модели) в простые типы для JSON, а в другую — принимает входной JSON, проверяет его и собирает обратно в проверенные данные или объект.

Зачем это знать на практике

Сериализатор — сердце любого DRF-проекта. Через него проходит всё: ответ клиенту формируется из объекта, а каждый POST и PUT сначала проверяется именно здесь. Если перепутать, где валидировать, или забыть про read_only, в API легко появляются дыры: клиент перезаписывает чужой id, в базу попадает отрицательная цена, а пароль случайно уезжает в ответе. Понимание сериализаторов превращает написание API из борьбы с ошибками 500 в спокойную работу с предсказуемыми 400.

Serializer против ModelSerializer

Базовый класс serializers.Serializer требует описать каждое поле вручную — он не знает про вашу модель. Это даёт полный контроль, но многословно.

from rest_framework import serializers

class ArticleSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    title = serializers.CharField(max_length=200)
    views = serializers.IntegerField(default=0)

    def create(self, validated_data):
        return Article.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.title = validated_data.get("title", instance.title)
        instance.save()
        return instance

Класс serializers.ModelSerializer делает то же самое, но выводит поля из модели и сам реализует create() и update(). В 90% случаев берут именно его.

class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = ["id", "title", "views", "author"]
        read_only_fields = ["views"]

В Meta.fields перечисляют поля явно — это лучше, чем fields = "__all__", потому что при добавлении в модель нового (например, служебного) поля оно не «протечёт» в API само по себе.

Два направления: представление и парсинг

На выход данные берут из объекта через атрибут .data:

serializer = ArticleSerializer(article)
serializer.data
# {"id": 1, "title": "DRF", "views": 42, "author": 7}

На вход данные принимают и обязательно проверяют. Без вызова is_valid() доступ к .validated_data запрещён:

serializer = ArticleSerializer(data=request.data)
if serializer.is_valid():
    serializer.save()          # вызовет create() или update()
else:
    serializer.errors          # словарь ошибок по полям

В реальном коде обычно пишут serializer.is_valid(raise_exception=True) — тогда DRF сам вернёт клиенту ответ 400 с телом ошибок.

read_only и write_only

Эти флаги решают, в какую сторону движется поле. read_only=True — поле уходит в ответ, но игнорируется на входе (клиент не может его задать). write_only=True — наоборот: поле принимается, но никогда не показывается в ответе. Классика — пароль.

class UserSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True)

    class Meta:
        model = User
        fields = ["id", "username", "password"]

    def create(self, validated_data):
        user = User(username=validated_data["username"])
        user.set_password(validated_data["password"])
        user.save()
        return user

Здесь password приходит в POST, хешируется и попадает в базу, но в JSON-ответе его не будет никогда. А id в ModelSerializer по умолчанию read_only — поэтому клиент физически не может подменить идентификатор записи.

Валидация полей и объекта

Валидацию в DRF разделяют на два уровня. Полевая проверяет одно поле — метод validate_<имя>:

def validate_title(self, value):
    if "спам" in value.lower():
        raise serializers.ValidationError("Заголовок содержит спам.")
    return value

Объектная проверяет связь нескольких полей — метод validate, в него приходит словарь уже проверенных полей:

def validate(self, data):
    if data["published_at"] < data["created_at"]:
        raise serializers.ValidationError(
            "Дата публикации не может быть раньше создания."
        )
    return data

Правило простое: проверка одного поля — в validate_<поле>, проверка зависимости между полями — в validate. Метод обязан вернуть значение (поле или весь словарь), иначе данные потеряются.

Вложенные сериализаторы

Чтобы вложить связанный объект целиком, поле объявляют как другой сериализатор. По умолчанию это даёт чтение вложенной структуры:

class AuthorSerializer(serializers.ModelSerializer):
    class Meta:
        model = Author
        fields = ["id", "name"]

class ArticleSerializer(serializers.ModelSerializer):
    author = AuthorSerializer(read_only=True)

    class Meta:
        model = Article
        fields = ["id", "title", "author"]

Результат — автор не как число, а как объект:

{
  "id": 1,
  "title": "DRF",
  "author": {"id": 7, "name": "Аня"}
}

Для коллекции добавляют many=True. А вот запись вложенных объектов «из коробки» не работает: если вложенный сериализатор не read_only, нужно вручную переопределить create()/update(), иначе DRF выбросит ошибку. Поэтому на вход чаще оставляют PrimaryKeyRelatedField (просто id связанного объекта), а вложенное представление включают только для ответа.

Как это работает под капотом

Внутри сериализатор — это набор объектов-полей (Field). На выход каждое поле через to_representation() достаёт своё значение из объекта и приводит к примитиву; собранный словарь и есть .data. На вход вызывается to_internal_value(): поле приводит сырое значение к нужному типу и запускает свои встроенные проверки (тип, max_length, required). Затем по очереди отрабатывают validate_<поле>, потом общий validate. Любая поднятая ValidationError не прерывает программу, а складывается в serializer.errors с привязкой к полю. Только пройдя весь конвейер, данные оказываются в validated_data, и лишь после этого save() зовёт create() или update(). Из-за этого порядка обращаться к validated_data до is_valid() бессмысленно — там ещё ничего нет.

Частые ошибки

  • Читать validated_data без is_valid(). DRF специально бросит исключение: данные ещё не проверены.
  • Класть пароль или токен в обычное поле. Без write_only=True секрет уедет клиенту в ответе.
  • Забыть return в методе валидации. Если validate/validate_<поле> ничего не возвращает, поле станет None.
  • Ожидать запись вложенных объектов автоматически. Writable-вложенность требует ручных create()/update(); на вход удобнее принимать id.
  • Ставить fields = "__all__". Новое поле модели автоматически попадёт в API, что чревато утечкой служебных данных.

Итоги

  • ModelSerializer выводит поля из модели и сам реализует create()/update(); Serializer описывают вручную.
  • На выход — .data, на вход — is_valid() и затем .validated_data; ошибки лежат в .errors.
  • read_only — только в ответ, write_only — только на вход (пароли).
  • Полевая проверка — validate_<поле>, объектная — validate; оба метода обязаны вернуть значение.
  • Вложенный сериализатор по умолчанию только для чтения; для записи нужны ручные методы.
Проверьте себя
1. Чем ModelSerializer отличается от базового Serializer?
AModelSerializer работает только с GET-запросами
BModelSerializer выводит поля из модели и сам реализует create()/update()
CSerializer быстрее, потому что не валидирует данные
DМежду ними нет разницы, это псевдонимы
2. Где правильно проверить, что published_at не раньше created_at (зависимость двух полей)?
AВ методе validate_published_at
BВ методе validate(self, data)
CВ Meta-классе
DВ функции create()
3. Какой флаг сделает поле password принимаемым на входе, но невидимым в ответе?
Aread_only=True
Brequired=False
Cwrite_only=True
Ddefault=None