Эффективное использование сериализаторов 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__'
Допустим, модель действительна только в том случае, если оба эти условия верны:
rating
is between 1 and 10us_gross
is 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 поле |