Эффективное использование сериализаторов Django REST Framework
В этой статье мы рассмотрим, как использовать сериализаторы Django REST Framework (DRF) более эффективно и действенно на примере. Попутно мы погрузимся в некоторые продвинутые концепции, такие как использование ключевого слова source, передача контекста, проверка данных и многое другое.
Эта статья предполагает, что вы уже имеете достаточное представление о Django REST Framework.
Что покрывается?
В этой статье рассматриваются:
- Проверка данных на уровне поля или объекта
- Настройка вывода сериализации и десериализации
- Передача дополнительных данных при сохранении
- Передача контекста сериализаторам
- Переименование полей вывода сериализатора
- Присоединение ответов функции сериализатора к данным
- Извлечение данных из моделей «один к одному»
- Присоединение данных к сериализованному выводу
- Создание отдельных сериализаторов чтения и записи
- Настройка полей только для чтения
- Обработка вложенной сериализации
Концепции, представленные в этой статье, не связаны друг с другом. Я рекомендую прочитать статью в целом, но не стесняйтесь сосредоточиться на концепции (концепциях), которые вас конкретно интересуют.
Валидация пользовательских данных
DRF обеспечивает проверку данных в процессе десериализации, поэтому перед доступом к проверенным данным необходимо вызвать is_valid(). Если данные недействительны, ошибки добавляются к свойству сериализатора error и возникает ошибка ValidationError.
Существует два типа пользовательских валидаторов данных:
- Custom field
- Object-level
Рассмотрим пример. Предположим, у нас есть Movie модель:
from django.db import models
class Movie(models.Model):
title = models.CharField(max_length=128)
description = models.TextField(max_length=2048)
release_date = models.DateField()
rating = models.PositiveSmallIntegerField()
us_gross = models.IntegerField(default=0)
worldwide_gross = models.IntegerField(default=0)
def __str__(self):
return f'{self.title}'
В нашей модели есть title, description, release_date, rating, us_gross и worldwide_gross.
У нас также есть простой ModelSerializer, который сериализует все поля:
from rest_framework import serializers
from examples.models import Movie
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
Допустим, модель действительна только в том случае, если оба эти условия верны:
ratingis between 1 and 10us_grossis less thanworldwide_gross
Для этого мы можем использовать пользовательские валидаторы данных.
Валидация пользовательского поля
Пользовательская проверка полей позволяет нам проверять конкретное поле. Мы можем использовать его, добавив метод validate_<field_name> в наш сериализатор следующим образом:
from rest_framework import serializers
from examples.models import Movie
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
def validate_rating(self, value):
if value < 1 or value > 10:
raise serializers.ValidationError('Rating has to be between 1 and 10.')
return value
Наш метод validate_rating позаботится о том, чтобы рейтинг всегда оставался между 1 и 10.
Валидация на уровне объекта
Иногда для проверки полей необходимо сравнить их друг с другом. В этом случае следует использовать подход валидации на уровне объекта.
Пример:
from rest_framework import serializers
from examples.models import Movie
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
def validate(self, data):
if data['us_gross'] > data['worldwide_gross']:
raise serializers.ValidationError('worldwide_gross cannot be bigger than us_gross')
return data
Метод validate гарантирует, что us_gross никогда не будет больше worldwide_gross.
Вам следует избегать доступа к дополнительным полям валидатора пользовательских полей через
self.initial_data. Этот словарь содержит необработанные данные, что означает, что ваши типы данных не обязательно будут соответствовать требуемым типам данных. DRF также будет добавлять ошибки валидации к неправильному полю.
Функциональные валидаторы
Если мы используем один и тот же валидатор в нескольких сериализаторах, мы можем создать валидатор функции вместо того, чтобы писать один и тот же код снова и снова. Давайте напишем валидатор, который проверяет, находится ли число между 1 и 10:
def is_rating(value):
if value < 1:
raise serializers.ValidationError('Value cannot be lower than 1.')
elif value > 10:
raise serializers.ValidationError('Value cannot be higher than 10')
Теперь мы можем добавить его к нашему MovieSerializer следующим образом:
from rest_framework import serializers
from examples.models import Movie
class MovieSerializer(serializers.ModelSerializer):
rating = IntegerField(validators=[is_rating])
...
Пользовательские выходы
Две наиболее полезные функции класса BaseSerializer, которые мы можем переопределить, это to_representation() и to_internal_value(). Переопределив их, мы можем изменить поведение сериализации и десериализации, соответственно, для добавления дополнительных данных, извлечения данных и обработки отношений.
to_representation()allows us to change the serialization outputto_internal_value()allows us to change the deserialization output
Предположим, что у вас есть следующая модель:
from django.contrib.auth.models import User
from django.db import models
class Resource(models.Model):
title = models.CharField(max_length=256)
content = models.TextField()
liked_by = models.ManyToManyField(to=User)
def __str__(self):
return f'{self.title}'
Каждый ресурс имеет поля title, content и liked_by. liked_by представляет пользователей, которым понравился ресурс.
Наш сериализатор определен следующим образом:
from rest_framework import serializers
from examples.models import Resource
class ResourceSerializer(serializers.ModelSerializer):
class Meta:
model = Resource
fields = '__all__'
Если мы сериализуем ресурс и обратимся к его data свойству, мы получим следующий результат:
{
"id": 1,
"title": "C++ with examples",
"content": "This is the resource's content.",
"liked_by": [
2,
3
]
}
to_representation()
Теперь, допустим, мы хотим добавить общее количество лайков к сериализованным данным. Самый простой способ добиться этого - реализовать метод to_representation в нашем классе сериализатора:
from rest_framework import serializers
from examples.models import Resource
class ResourceSerializer(serializers.ModelSerializer):
class Meta:
model = Resource
fields = '__all__'
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['likes'] = instance.liked_by.count()
return representation
Этот фрагмент кода получает текущее представление, добавляет к нему likes и возвращает его.
Если мы сериализуем другой ресурс, мы получим следующий результат:
{
"id": 1,
"title": "C++ with examples",
"content": "This is the resource's content.",
"liked_by": [
2,
3
],
"likes": 2
}
to_internal_value()
{
"info": {
"extra": "data",
...
},
"resource": {
"id": 1,
"title": "C++ with examples",
"content": "This is the resource's content.",
"liked_by": [
2,
3
],
"likes": 2
}
}
Предположим, что сервисы, использующие наш API, добавляют ненужные данные в конечную точку при создании ресурсов:
Если мы попытаемся сериализовать эти данные, наш сериализатор потерпит неудачу, потому что он не сможет извлечь ресурс.
Мы можем переопределить to_internal_value() для извлечения данных о ресурсах:
from rest_framework import serializers
from examples.models import Resource
class ResourceSerializer(serializers.ModelSerializer):
class Meta:
model = Resource
fields = '__all__'
def to_internal_value(self, data):
resource_data = data['resource']
return super().to_internal_value(resource_data)
Ура! Наш сериализатор теперь работает так, как ожидалось.
Сериализатор save
Вызов save() приведет либо к созданию нового экземпляра, либо к обновлению существующего, в зависимости от того, был ли передан существующий экземпляр при создании класса сериализатора:
# this creates a new instance
serializer = MySerializer(data=data)
# this updates an existing instance
serializer = MySerializer(instance, data=data)
Передача данных непосредственно в сохранение
Иногда требуется передать дополнительные данные в момент сохранения экземпляра. Эти дополнительные данные могут включать такую информацию, как текущий пользователь, текущее время или данные запроса.
Вы можете сделать это, включив дополнительные аргументы ключевых слов при вызове save(). Например:
serializer.save(owner=request.user)
Имейте в виду, что значения, переданные в
save(), не будут проверены.
Контекст сериализатора
В некоторых случаях необходимо передавать дополнительные данные в сериализаторы. Это можно сделать с помощью свойства сериализатора context. Затем вы можете использовать эти данные внутри сериализатора, например, to_representation или при проверке данных.
Вы передаете данные в виде словаря через ключевое слово context:
from rest_framework import serializers
from examples.models import Resource
resource = Resource.objects.get(id=1)
serializer = ResourceSerializer(resource, context={'key': 'value'})
Затем вы можете получить его внутри класса сериализатора из словаря self.context следующим образом:
from rest_framework import serializers
from examples.models import Resource
class ResourceSerializer(serializers.ModelSerializer):
class Meta:
model = Resource
fields = '__all__'
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['key'] = self.context['key']
return representation
Вывод сериализатора теперь будет содержать key с value.
Источник ключевого слова
Сериализатор DRF поставляется с ключевым словом source, которое является чрезвычайно мощным и может быть использовано в нескольких сценариях. Мы можем использовать его для:
- Переименовать поля вывода сериализатора
- Прикрепить ответ функции сериализатора к данным
- Извлечение данных из моделей «один к одному»
Допустим, вы создаете социальную сеть, и у каждого пользователя есть своя UserProfile, которая имеет связь один-к-одному с моделью User:
from django.contrib.auth.models import User
from django.db import models
class UserProfile(models.Model):
user = models.OneToOneField(to=User, on_delete=models.CASCADE)
bio = models.TextField()
birth_date = models.DateField()
def __str__(self):
return f'{self.user.username} profile'
Мы используем ModelSerializer для сериализации наших пользователей:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'is_staff', 'is_active']
Давайте сериализуем пользователя:
{
"id": 1,
"username": "admin",
"email": "admin@admin.com",
"is_staff": true,
"is_active": true
}
Переименовать выходные поля сериализатора
Чтобы переименовать выходное поле сериализатора, нам нужно добавить новое поле в наш сериализатор и передать его в fields свойство.
class UserSerializer(serializers.ModelSerializer):
active = serializers.BooleanField(source='is_active')
class Meta:
model = User
fields = ['id', 'username', 'email', 'is_staff', 'active']
Теперь наше активное поле будет называться active вместо is_active.
Присоедините ответ функции сериализатора к данным
Мы можем использовать source для добавления поля, равного возврату функции.
class UserSerializer(serializers.ModelSerializer):
full_name = serializers.CharField(source='get_full_name')
class Meta:
model = User
fields = ['id', 'username', 'full_name', 'email', 'is_staff', 'active']
get_full_name()- это метод из модели пользователя Django, который конкатенируетuser.first_nameиuser.last_name.
Теперь наш ответ будет содержать full_name.
Добавьте данные из моделей "один к одному"
Теперь предположим, что мы также хотим включить данные нашего пользователя bio и birth_date в UserSerializer. Мы можем сделать это, добавив дополнительные поля в наш сериализатор с помощью ключевого слова source.
Давайте изменим наш класс сериализатора:
class UserSerializer(serializers.ModelSerializer):
bio = serializers.CharField(source='userprofile.bio')
birth_date = serializers.DateField(source='userprofile.birth_date')
class Meta:
model = User
fields = [
'id', 'username', 'email', 'is_staff',
'is_active', 'bio', 'birth_date'
] # note we also added the new fields here
Мы можем получить доступ к userprofile.<field_name>, потому что это отношение один-к-одному с нашим пользователем.
Это наш окончательный ответ в формате JSON:
{
"id": 1,
"username": "admin",
"email": "",
"is_staff": true,
"is_active": true,
"bio": "This is my bio.",
"birth_date": "1995-04-27"
}
SerializerMethodField
SerializerMethodField - это поле только для чтения, которое получает свое значение путем вызова метода класса сериализатора, к которому оно присоединено. Его можно использовать для присоединения любого типа данных к сериализованному представлению объекта.
SerializerMethodField получает свои данные, вызывая get_<field_name>.
Если бы мы хотели добавить атрибут full_name к нашему сериализатору User, мы могли бы добиться этого следующим образом:
from django.contrib.auth.models import User
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
full_name = serializers.SerializerMethodField()
class Meta:
model = User
fields = '__all__'
def get_full_name(self, obj):
return f'{obj.first_name} {obj.last_name}'
Этот фрагмент кода создает пользовательский сериализатор, который также содержит full_name, являющийся результатом функции get_full_name().
Различные сериализаторы чтения и записи
Если ваши сериализаторы содержат много вложенных данных, которые не требуются для операций записи, вы можете повысить производительность API, создав отдельные сериализаторы для чтения и записи.
Вы делаете это, переопределяя метод get_serializer_class() в вашем ViewSet следующим образом:
from rest_framework import viewsets
from .models import MyModel
from .serializers import MyModelWriteSerializer, MyModelReadSerializer
class MyViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
def get_serializer_class(self):
if self.action in ["create", "update", "partial_update", "destroy"]:
return MyModelWriteSerializer
return MyModelReadSerializer
Этот код проверяет, какая операция REST была использована, и возвращает MyModelWriteSerializer для операций записи и MyModelReadSerializer для операций чтения.
Поля только для чтения
Поля сериализатора поставляются с опцией read_only. Если установить значение True, DRF включает поле в вывод API, но игнорирует его во время операций создания и обновления:
from rest_framework import serializers
class AccountSerializer(serializers.Serializer):
id = IntegerField(label='ID', read_only=True)
username = CharField(max_length=32, required=True)
Установка таких полей, как
id,create_dateи т.д., только для чтения даст вам прирост производительности при операциях записи.
Если вы хотите установить несколько полей в read_only, вы можете указать их с помощью read_only_fields в Meta следующим образом:
from rest_framework import serializers
class AccountSerializer(serializers.Serializer):
id = IntegerField(label='ID')
username = CharField(max_length=32, required=True)
class Meta:
read_only_fields = ['id', 'username']
Вложенные сериализаторы
Существует два различных способа обработки вложенной сериализации с помощью ModelSerializer:
- Явное определение
- Использование поля
depth
Явное определение
Явное определение работает путем передачи внешнего Serializer в качестве поля нашему основному сериализатору.
Давайте рассмотрим пример. У нас есть Comment, который определяется следующим образом:
from django.contrib.auth.models import User
from django.db import models
class Comment(models.Model):
author = models.ForeignKey(to=User, on_delete=models.CASCADE)
datetime = models.DateTimeField(auto_now_add=True)
content = models.TextField()
Допустим, у вас есть следующий сериализатор:
from rest_framework import serializers
class CommentSerializer(serializers.ModelSerializer):
author = UserSerializer()
class Meta:
model = Comment
fields = '__all__'
Если мы сериализуем Comment, то получим следующий вывод:
{
"id": 1,
"datetime": "2021-03-19T21:51:44.775609Z",
"content": "This is an interesting message.",
"author": 1
}
Если мы также хотим сериализовать пользователя (вместо того, чтобы показывать только его ID), мы можем добавить поле сериализатора author к нашему Comment:
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username']
class CommentSerializer(serializers.ModelSerializer):
author = UserSerializer()
class Meta:
model = Comment
fields = '__all__'
Снова просериализуйте, и вы получите следующее:
{
"id": 1,
"author": {
"id": 1,
"username": "admin"
},
"datetime": "2021-03-19T21:51:44.775609Z",
"content": "This is an interesting message."
}
Использование поля глубины
Когда речь идет о вложенной сериализации, поле depth является одной из самых мощных возможностей. Предположим, у нас есть три модели - ModelA, ModelB и ModelC. ModelA зависит от ModelB, а ModelB зависит от ModelC. Они определяются следующим образом:
from django.db import models
class ModelC(models.Model):
content = models.CharField(max_length=128)
class ModelB(models.Model):
model_c = models.ForeignKey(to=ModelC, on_delete=models.CASCADE)
content = models.CharField(max_length=128)
class ModelA(models.Model):
model_b = models.ForeignKey(to=ModelB, on_delete=models.CASCADE)
content = models.CharField(max_length=128)
Наш сериализатор ModelA, который является объектом верхнего уровня, выглядит следующим образом:
from rest_framework import serializers
class ModelASerializer(serializers.ModelSerializer):
class Meta:
model = ModelA
fields = '__all__'
Если мы сериализуем объект примера, то получим следующий результат:
{
"id": 1,
"content": "A content",
"model_b": 1
}
Допустим, мы также хотим включить содержимое ModelB при сериализации ModelA. Мы можем добавить явное определение к нашему ModelASerializer или использовать поле depth.
Когда мы меняем depth на 1 в нашем сериализаторе следующим образом:
from rest_framework import serializers
class ModelASerializer(serializers.ModelSerializer):
class Meta:
model = ModelA
fields = '__all__'
depth = 1
Вывод меняется на следующий:
{
"id": 1,
"content": "A content",
"model_b": {
"id": 1,
"content": "B content",
"model_c": 1
}
}
Если мы изменим его на 2 наш сериализатор будет сериализовать на уровень глубже:
{
"id": 1,
"content": "A content",
"model_b": {
"id": 1,
"content": "B content",
"model_c": {
"id": 1,
"content": "C content"
}
}
}
Недостатком является то, что у вас нет контроля над сериализацией дочернего объекта. Использование
depthбудет включать все поля дочерних элементов, другими словами.
Заключение
В этой статье вы узнали ряд различных советов и приемов для более эффективного использования сериализаторов DRF.
Кратко о том, что конкретно мы рассмотрели:
| Концепция | Метод |
|---|---|
| Валидация данных на уровне поля или объекта | validate_<field_name> или validate |
| Настройка вывода сериализации и десериализации | to_representation и to_internal_value |
| Передача дополнительных данных при сохранении | serializer.save(additional=data) |
| Передача контекста сериализаторам | SampleSerializer(resource, context={'key': 'value'}) |
| Переименование выходных полей сериализатора | source ключевое слово |
| Присоединение ответов функций сериализатора к данным | source ключевое слово |
| Получение данных из моделей "один к одному" | source ключевое слово |
| Присоединение данных к сериализованному выводу | SerializerMethodField |
| Создание отдельных сериализаторов для чтения и записи | get_serializer_class() |
| Установка полей, доступных только для чтения | read_only_fields |
| Обработка вложенной сериализации | depthполе |