Эффективное использование сериализаторов Django REST Framework

В этой статье мы рассмотрим, как использовать сериализаторы Django REST Framework (DRF) более эффективно и действенно на примере. Попутно мы погрузимся в некоторые продвинутые концепции, такие как использование ключевого слова source, передача контекста, проверка данных и многое другое.

Эта статья предполагает, что вы уже имеете достаточное представление о Django REST Framework.

Что покрывается?

В этой статье рассматриваются:

  1. Проверка данных на уровне поля или объекта
  2. Настройка вывода сериализации и десериализации
  3. Передача дополнительных данных при сохранении
  4. Передача контекста сериализаторам
  5. Переименование полей вывода сериализатора
  6. Присоединение ответов функции сериализатора к данным
  7. Извлечение данных из моделей «один к одному»
  8. Присоединение данных к сериализованному выводу
  9. Создание отдельных сериализаторов чтения и записи
  10. Настройка полей только для чтения
  11. Обработка вложенной сериализации

Концепции, представленные в этой статье, не связаны друг с другом. Я рекомендую прочитать статью в целом, но не стесняйтесь сосредоточиться на концепции (концепциях), которые вас конкретно интересуют.

Валидация пользовательских данных

DRF обеспечивает проверку данных в процессе десериализации, поэтому перед доступом к проверенным данным необходимо вызвать is_valid(). Если данные недействительны, ошибки добавляются к свойству сериализатора error и возникает ошибка ValidationError.

Существует два типа пользовательских валидаторов данных:

  1. Custom field
  2. 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__'

Допустим, модель действительна только в том случае, если оба эти условия верны:

  1. rating is between 1 and 10
  2. us_gross is less than worldwide_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(). Переопределив их, мы можем изменить поведение сериализации и десериализации, соответственно, для добавления дополнительных данных, извлечения данных и обработки отношений.

  1. to_representation() allows us to change the serialization output
  2. to_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, которое является чрезвычайно мощным и может быть использовано в нескольких сценариях. Мы можем использовать его для:

  1. Переименовать поля вывода сериализатора
  2. Прикрепить ответ функции сериализатора к данным
  3. Извлечение данных из моделей «один к одному»

Допустим, вы создаете социальную сеть, и у каждого пользователя есть своя 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:

  1. Явное определение
  2. Использование поля 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поле
Вернуться на верх