Сериализаторы: преобразование и валидация
Разбираем, как сериализатор превращает объекты 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; оба метода обязаны вернуть значение. - Вложенный сериализатор по умолчанию только для чтения; для записи нужны ручные методы.