В предыдущих статьях мы подробно разобрали работу сериалайзера на основе классов BaseSerializer
и Serializer
, и теперь мы можем перейти к классу-наследнику ModelSerializer
.
Класс модельных сериалайзеров отличается лишь тем, что у него есть несколько инструментов, позволяющих сократить код сериалайзера:
- автоматическое создание полей сериалайзера на основе данных о корреспондирующих полях модели;
- автоматическое включение в поля сериалайзера тех же валидаторов, которые есть в полях модели, а также при определённых условиях метавалидаторов;
- заранее определённые методы
create
иupdate
.
Общие принципы работы модельного сериалайзера как на чтение, так и на запись идентичны тому, как работает базовый класс Serializer
.
ModelSerializer: необходимый минимум
Для определения модельного сериалайзера нужен внутренний класс Meta
со следующими атрибутами:
-
model
— это джанго-модель, которую будет обслуживать сериалайзер. Модель не должна быть абстрактной. -
Один из следующих атрибутов:
-
fields
— поля джанго-модели, для которых будут созданы корреспондирующие поля сериалайзера; -
exclude
— поля джанго-модели, для которых не нужно создавать поля сериалайзера, но они будут созданы для всех остальных.
-
Автоматически создаваемые и декларируемые поля
Автоматически создаваемые поля. Поля сериалайзера, имена которых указаны во внутреннем классе Meta
явно через fields
или неявно через exclude
, DRF создаст самостоятельно, сопоставив с одноимёнными полями модели. Если одноимённого поля модели не найдётся, будет выброшено исключение через метод build_unknown_field
.
Декларируемые поля. Это обычные поля сериалайзера, которые мы описываем самостоятельно вне класса Meta
, как делали это в предыдущих статьях, например, в статье о создании сериалайзера, работающего на запись.
class TownSerializer(serializers.ModelSerializer):
name = serializers.CharField(allow_blank=True)
class Meta:
model = Town
fields = ['id', 'writers', 'name']
Поле name
— декларируемое, класс поля и его атрибуты мы задали самостоятельно. Поля id
и writers
— автоматически создаваемые.
Обратите внимание, что name
также включено в список fields
. Дело в том, что, если присутствует атрибут fields
, в нём должны быть перечислены все поля сериалайзера, в том числе и декларируемые.
Декларируемые поля могут понадобится, когда нам не подходит класс поля, который модельный сериалайзер подберёт автоматически. А ещё через декларируемые поля зачастую используют вспомогательные классы ReadOnlyField
, HiddenField
, SerializerMethodField
.
Если вас устраивает класс поля сериалайзера, но нужна тонкая настройка его параметров, включая создание нескольких полей сериалайзера для одного поля модели, скорее всего, будет достаточно воспользоваться атрибутом extra_kwargs
внутреннего класса Meta
.
Что означает fields = '__all__'
Если в качестве значения fields
выступает строка __all__
, то в сериалайзере будут созданы поля для обслуживания всех полей модели, кроме тех, за работу с которыми будут отвечать декларируемые поля сериалайзера. Использование __all__
отменяет необходимость прописывать в fields
декларируемые поля.
Как правильно использовать fields
и exclude
На основе исходного кода DRF можно сформулировать несколько правил.
-
fields
иexclude
нельзя использовать вместе. -
exclude
должен быть либо списком, либо кортежем из названий полей модели, даже если поле одно. -
fields
может быть задан в виде списка либо кортежа (даже если поле одно) или строки__all__
.
Примеры модельного сериалайзера для джанго-модели Writer
:
class Writer(models.Model):
firstname = models.CharField(max_length=100...)
lastname = models.CharField(max_length=100...)
patronymic = models.CharField(max_length=100...)
birth_place = models.ForeignKey(to=Town...)
birth_date = models.DateField(...)
Сериалайзер, который будет обслуживать все поля модели:
class WriterModelSerializer(serializers.ModelSerializer):
class Meta:
model = Writer
fields = '__all__'
Сериалайзер, который будет обслуживать только поля firstname
и lastname
:
class WriterModelSerializer(serializers.ModelSerializer):
class Meta:
model = Writer
fields = ['firstname', 'lastname']
Сериалайзер, который будет обслуживать все поля, кроме firstname
и lastname
:
class WriterModelSerializer(serializers.ModelSerializer):
class Meta:
model = Writer
exclude = ['firstname', 'lastname']
Как DRF создаёт поля для модельного сериалайзера
Сбор информации о модели и распределение полей по группам. Сначала DRF собирает детальную информацию о полях модели. Для этого он задействует метод get_field_info
из rest_framework.utils.model_meta
. Результат — именованный кортеж FieldResult
, в котором есть следующие элементы:
-
relations
— словарь, объединяющий поля отношений модели("forward_relations")
, а также объекты классаRelatedManager ("reverse_relations")
; -
fields_and_pk
— словарь с остальными полями модели.
Для подбора корреспондирующих полей DRF использует три метода:
-
build_relational_field
— для полей изrelations
; -
build_property_field
— для методов и property-атрибутов модели; -
build_standard_field
— для полей изfields_and_pk
.
Рассмотрим на примерах:
class Town(models.Model):
name = models.CharField(max_length=100, unique=True)
class Writer(models.Model):
firstname = models.CharField(max_length=100...)
lastname = models.CharField(max_length=100...)
patronymic = models.CharField(max_length=100...)
birth_place = models.ForeignKey(to=Town, to_field='name', related_name='writers'...)
birth_date = models.DateField(...)
def get_full_name(self):
return f'{self.firstname} {self.patronymic} {self.lastname}'
Из модели Town
DRF возьмёт:
- поле
name
и поместит его вfields_and_pk
для дальнейшей обработки методомbuild_standard_field
; - менеджер
writers
, который связывает модельTown
с модельюWriter
.writers
— это значение атрибутаrelated_name
поляbirth_place
в связанной модели. Менеджер будет помещён вrelations
и в дальнейшем обработан методомbuild_relational_field
или методомbuild_nested_field
.
Из модели Writer
DRF возьмёт:
- поля
firstname
,lastname
,patronymic
,birth_date
и поместит их вfields_and_pk
для дальнейшей обработки методомbuild_standard_field
; - поле
birth_place
, которое поместит вrelations
для дальнейшей обработки методомbuild_relational_field
или методомbuild_nested_field
; - метод
get_full_name
, который переадресует методуbuild_property_field
после проверкиhasattr(model_class, field_name)
.
Подбор классов полей сериалайзера для стандартных полей модели. Стандартные поля модели — это поля, которые не относятся к полям отношений. В классе ModelSerializer
есть атрибут, по которому DRF решает, какой класс поля сериалайзера подобрать для конкретного поля модели. Это атрибут serializer_fields_mapping
. При необходимости его можно дополнить или переопределить.
По дефолту классы полей модели и сериалайзера сопоставляются так:
№ | Класс поля сериалайзера | Класс поля модели |
---|---|---|
1 | BooleanField | BooleanField, NullBooleanField |
2 | CharField | CharField, TextField |
3 | DateField | DateField |
4 | DateTimeField | DateTimeField |
5 | DecimalField | DecimalField |
6 | DurationField | DurationField |
7 | EmailField | EmailField |
8 | FileField | FileField |
9 | FilePathField | FilePathField |
10 | FloatField | FloatField |
11 | ImageField | ImageField |
12 | IPAddressField | GenericIPAddressField |
13 | IntegerField | AutoField, BigIntegerField, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, SmallIntegerField |
14 | SlugField | SlugField |
15 | TimeField | TimeField |
16 | URLField | URLField |
17 | UUIDField | UUIDField |
Вне serializer_fields_mapping
описана логика сопоставления полей JSONField
, а также специфичных полей для PostgreSQL.
Подбор классов для полей отношений. Для ForeignKey-полей модели в модельном сериалайзере могут создаваться поля одного из двух классов:
-
SlugRelatedField
, если в поле модели задан атрибутto_field
; -
PrimaryKeyRelatedField
во всех иных случаях.
Для полей ManyToMany и обратных связей (объектов RelatedManager
) создаётся поле класса PrimaryKeyRelatedField
.
Важно: названия автоматически создаваемых полей сериалайзера для обратных связей нужно явно указывать в атрибуте fields
класса Meta
. Строка __all__
не включает в себя названия объектов RelatedManager-модели.
class Town(models.Model):
name = models.CharField(max_length=100, unique=True)
class Writer(models.Model):
# другие поля опускаем для краткости
birth_place = models.ForeignKey(to=Town, to_field='name', related_name='writers',...)
class TownSerializer(serializers.ModelSerializer):
class Meta:
model = Town
fields = '__all__' # в сериалайзере будет создано два поля: `id` и `name`
#ИЛИ fields = ['id', 'name', 'writers'] — будет дополнительно создано поле `writers`
# для доступа к записям в связанной модели
class WriterSerializer(serialziers.ModelSerializer):
class Meta:
model = Writer
fields = '__all__' # в сериалайзере будут все поля модели, включая `birth_place`,
# т.к. `forward_relations` включаются в `__all__`.
# для поля `birth_place` будет подобран класс `SlugRelatedField`, поскольку
# в корреспондирующем поле модели установлен атрибут `to_field`.
Работа с методами модели и проперти. В этом случае ничего особенного не происходит: DRF создаёт ReadOnlyField
, которое участвует только при работе сериалайзера на чтение. Без какой-либо дополнительной валидации оно возвращает значение из метода или проперти модели.
Работа с вложенными объектами
Атрибут depth
Для примера возьмем сериалайзер из предыдущего раздела и используем его для чтения первой записи из модели:
class TownSerializer(serializers.ModelSerializer):
class Meta:
model = Town
fields = ['id', 'name', 'writers']
s = TownSerializer(instance=Town.objects.first())
print(s.data)
-------------
{'id': 1, 'name': 'Вологда', 'writers': [6, 7]}
По ключу writers
мы получили список из айдишников писателей, которые родились в Вологде.
Если мы хотим раскрыть информацию о вложенных объектах, поможет атрибут depth
класса Meta
. Устанавливаем ему значение 1, и тогда сериалайзер раскрывает содержимое объектов из списка writers
.
class TownSerializer(serializers.ModelSerializer):
class Meta:
model = Town
depth = 1
fields = ['id', 'name', 'writers']
s = TownSerializer(instance=Town.objects.first())
print(s.data)
-------------
{
'id': 1,
'name': 'Вологда',
'writers': [
{'id': 6, 'firstname': 'Варлам', 'lastname': 'Шаламов', ...},
{'id': 7, 'firstname': 'Константин', 'lastname': 'Батюшков', ...}
]
}
Если установлен атрибут depth=1
, включается метод build_nested_field
. Тогда поле сериалайзера, которое отвечает за поле отношения модели или объект RelatedManager
, становится объектом класса NestedSerializer
. Его код очень прост:
class NestedSerializer(ModelSerializer):
class Meta:
model = relation_info.related_model
depth = nested_depth - 1
fields = '__all__'
Фактически объект из связанной модели обрабатывает сериалайзер внутри сериалайзера и выдаёт словарь со всеми полями этого объекта — fields = '__all__'
.
Вполне возможно, что все поля связанных объектов не нужны. Выход — явно указать, какой сериалайзер должен обслуживать прямые или обратные связи модели.
Сериалайзер в качестве поля
Для более гибкой работы с вложенными объектами можно передавать их сериалайзеру внутри сериалайзера самостоятельно, а не через объявление атрибута depth
в классе Meta
.
Для этого нужно сделать три шага.
Шаг первый. Поле, которое будет работать с вложенными объектами, нужно объявить явно в declared fields
— как самостоятельный атрибут сериалайзера.
Возвращаясь к примеру из предыдущего раздела, нам нужно вытащить поле writers
в декларируемые поля. Не забываем, что его нужно упомянуть в fields
в Meta
.
class TownSerializer(serializers.ModelSerializer):
# область декларируемых полей
writers = …
class Meta:
model = Town
fields = ['id', 'name', 'writers']
Шаг второй. Нужно создать или взять имеющийся сериалайзер, который будет обрабатывать вложенные объекты.
В нашем примере поле writers
будет иметь дело с объектами из модели Writer
. Создадим для них сериалайзер, который будет отдавать только имя, фамилию, отчество автора и дату его рождения. Иными словами, мы исключим поля id
и birth_place
.
class WriterSerializer(serializers.ModelSerializer):
class Meta:
model = Writer
exclude = ('id', 'birth_place')
Шаг третий. Остаётся передать созданный сериалайзер в качестве поля в TownSerializer
.
class TownSerializer(serializers.ModelSerializer):
writers = WriterSerializer(many=True) # Добавили сериалайзер в качестве поля
class Meta:
model = Town
fields = ['id', 'name', 'writers']
Важно: мы используем many=True
, потому что writers
— это всегда список айдишников авторов со связью «один-ко-многим».
Теперь, когда TownSerializer
вместе с остальными данными получит из записи в БД список writers
, он передаст его вложенному сериалайзеру WriterSerializer
. Он обработает каждый объект в списке и вернёт список словарей с интересующей нас информацией об авторах, которые родились в конкретном городе.
extra_kwargs
: тонкая настройка автоматически создаваемых полей
Атрибут extra_kwargs
определяют во внутреннем классе Meta
. Это словарь, ключами которого выступают поля из fields
или любого поля модели из тех, которые не перечислены в exclude
. Значением для каждого ключа служит словарь с атрибутами, которыми нужно дополнить то или иное поле сериалайзера.
Допустим, мы хотим, чтобы сериалайзер для модели Town
работал так:
- на чтение возвращал названия городов из столбца
name
не в виде{'name': 'название_города'}
, а в виде{'town': 'название_города'}
; - на запись получал данные для столбца
name
в виде{'name': 'название_города'}
.
По сути, нам нужно, чтобы одно и то же поле модели обслуживали поля сериалайзера с разными названиями — в зависимости от того, в какую сторону работает сериалайзер.
Вооружившись знаниями из предыдущих статей о том, как работает сериалайзер на чтение и на запись, можно прийти к такому решению:
class Town(models.Model):
name = models.CharField(max_length=100, unique=True)
class TownModelSerializer(serializers.ModelSerializer):
class Meta:
model = Town
fields = ['town', 'name']
extra_kwargs = {
'town': {'source': 'name', 'read_only': True},
'name': {'write_only': True}
}
TownModelSerializer(instance=Town.objects.first()).data
вернёт {'town': 'Вологда'}
.
TownModelSerializer(data={'name': 'Анапа'})
после валидации вернёт в validated_data
словарь {'name': 'Анапа'}
.
Итак, какую донастройку модельного сериалайзера мы провели:
- указали, что одноимённое с полем модели поле
name
работает только на запись. В данных, получаемых из базы, ключаname
не будет; - добавили сериалайзеру ещё одно поле под названием
town
и установили, что оно:- работает только на чтение, потому что только при получении словаря с данными о записи в модели
Town
там будет ключtown
; - источником (source) значения для этого ключа будет атрибут (поле)
name
записи в моделиTown
.
- работает только на чтение, потому что только при получении словаря с данными о записи в модели
Отмечу, что read_only_fields
можно задать и в качестве отдельного атрибута внутри Meta
, но обязательно в виде списка или кортежа.
Пример показывает, насколько гибким может быть сериалайзер и что не следует ограничивать осмысление DRF схемой «количество полей сериалайзера = количество полей модели». С одним и тем же полем модели может работать несколько полей сериалайзера и даже несколько разных сериалайзеров.
Важно: некоторые атрибуты полей не имеет смысла сочетать, а иногда это даже может привести к появлению исключения. В классе модельных сериалайзеров за правильным сочетанием атрибутов полей следит метод include_extra_kwargs
.
Не нужно устанавливать:
- атрибуты
required
,default
,allow_blank
,min_length
,max_length
,min_value
,max_value
,validators
,queryset
с атрибутомread_only=True
. DRF обрежет эти атрибуты, оставив толькоread_only;
- атрибут
required
с атрибутомdefault
, в котором есть какое-либо truthy-значение. DRF удалитrequired
, оставивdefault
.
Особенности валидации в ModelSerializer
Метавалидаторы unique_together
, unique_for_date
, unique_for_month
, unique_for_year
. При наличии таких валидаторов в модели DRF автоматически перенесёт их в сериалайзер. За это отвечает метод get_validators
. Но здесь есть два подводных камня.
- Никакого автоматического переноса не будет, если во внутреннем классе
Meta
задан атрибутvalidators
.
Иными словами, нельзя полагаться на то, что можно указать вvalidators
: например, если указать кастомный метавалидатор, к нему автоматом не подтянутся рассматриваемые метавалидаторы из модели.
Если вы решили использовать атрибутvalidators
, значит, нужно указывать в нём все метавалидаторы, включая те, которые уже есть в модели. -
unique_together
в настоящее время не рекомендован для использования в джанго-моделях. Вместо него документация советует использовать опциюconstraints
и классUniqueConstraint
.
Если вы следуете рекомендации, то соответствующий валидатор нужно перенести в сериалайзер вручную, потому что DRF автоматически этого не сделает.
Валидаторы на уровне поля. Валидаторы, как из параметра validators
, так и из специальных параметров, за которыми стоят различные виды валидаторов, например аргументы unique
, max_value
, min_value
, автоматически переносятся в поля сериалайзера. За это отвечает метод get_field_kwargs
из restframework.utils.field_mapping
.
Важно: сказанное относится только к автоматически создаваемым полям. Для декларируемых полей все валидаторы нужно указывать вручную.
На этом я завершаю цикл статей о том, как устроены сериалайзеры в Django REST Framework. Надеюсь, что этот материал позволит заложить крепкий фундамент в понимании одной из важнейших частей фреймворка и поможет создавать отлично работающие API в ваших проектах.
Все статьи серии:
Django Rest Framework для начинающих: создаём API для чтения данных: часть 1 и часть 2
Django Rest Framework для начинающих: создаём API для записи и обновления данных: часть 1 и часть 2