Введение в сериализацию. Класс Serializer

Курс по Django: https://stepik.org/a/183363

На предыдущем занятии мы с вами создали простой API-запрос, используя только класс представления. На этом занятии добавим ключевой компонент Django REST Framework в наш проект под названием сериализатор (Serializer).

Вначале ответим на вопрос, что это такое и зачем он вообще нужен? Как я уже отмечал, при реализации API сайта обмен данными выполняется посредством определенного формата. Чаще всего используют JSON кодирование, реже XML. При необходимости можно описать свой формат обмена данными. Правда, я лично с таким никогда не сталкивался. В 99% случаях все же применяется JSON. Так вот, роль сериализатора выполнять конвертирование произвольных объектов языка Python в формат JSON (в том числе модели фреймворка Django и наборы QuerySet). И, обратно, из JSON – в соответствующие объекты Python. (Полагая, что используется JSON в API-запросах).

Чтобы лучше понять, как реализована сериализация объектов, давайте для примера объявим некий класс WomenModel (в файле women/serializers.py), который будет имитировать класс модели фреймворка Django:

class WomenModel:
    def __init__(self, title, content):
        self.title = title
        self.content = content

А далее, класс WomenSerializer, который будет отвечать за сериализацию (то есть, за преобразование в формат JSON и обратно) объектов класса WomenModel:

class WomenSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=255)
    content = serializers.CharField()

Смотрите, мы здесь указываем, какие выбирать атрибуты (title и content) из объекта класса WomenModel для их перевода в JSON-формат и обратно, при получении JSON-данных они будут автоматически распакованы по атрибутам title и content. Причем, здесь же в сериализаторе мы прописываем проверку (валидацию) данных. Например, поле title не должно превышать 255 символов.

Давайте посмотрим, как все это будет работать. Запишем следующую тестовую функцию:

from rest_framework.renderers import JSONRenderer
 
 
def encode():
    model = WomenModel('Angelina Jolie', 'Content: Angelina Jolie')
    model_sr = WomenSerializer(model)
    print(model_sr.data, type(model_sr.data), sep='\n')
    json = JSONRenderer().render(model_sr.data)
    print(json, type(json), sep='\n')

Здесь создается объект-модель класса WomenModel, затем, он сериализуется путем создания объекта класса WomenSerializer, на вход которого мы передаем объект model. Далее, объект сериализованных данных преобразуется в обычную JSON-строку и результат выводится в консоль.

Переходим, теперь, на вкладку «Терминал», запускаем оболочку:

python manage.py shell

импортируем и запускаем нашу функцию:

from women.serializers import encode
encode()

На выходе видим строки:

{'title': 'Angelina Jolie', 'content': 'Content: Angelina Jolie'}
<class 'rest_framework.utils.serializer_helpers.ReturnDict'>
b'{"title":"Angelina Jolie","content":"Content: Angelina Jolie"}'
<class 'bytes'>

То есть, вначале у нас получается объект сериализации, а потом уже мы его преобразуем в обычную байтовую строку. Вот так, на автомате базовый класс Serializer выполнил преобразование объекта класса WomenModel в формат JSON.

Давайте теперь посмотрим на обратный процесс преобразование из JSON-строки в объект. Для этого определим функцию decode:

from rest_framework.parsers import JSONParser
 
 
def decode():
    stream = io.BytesIO(b'{"title":"Angelina Jolie","content":"Content: Angelina Jolie"}')
    data = JSONParser().parse(stream)
    serializer = WomenSerializer(data=data)
    serializer.is_valid()
    print(serializer.validated_data)

Здесь мы, как бы, читаем из входного потока данные в виде JSON-строки, затем, подаем все на JSONParser и снова создаем объект класса WomenSerializer, который возвратит нам ссылку на упорядоченный словарь. Следующей строчкой проверяем данные на корректности и выводим в консоль проверенные данные.

Посмотрим, как все это работает. Снова запустим оболочку:

python manage.py shell

Импортируем функцию decode и запустим ее:

from women.serializers import decode
decode()

Видим в консоли строку:

OrderedDict([('title', 'Angelina Jolie'), ('content', 'Content: Angelina Jolie')])

Это результат парсинга исходной JSON-строки в объект OrderedDict языка Python. Подробнее о работе процесса сериализации можно почитать на странице официальной документации:

https://www.django-rest-framework.org/api-guide/serializers/

Давайте теперь применим эту технику непосредственно для наших классов моделей. Вначале определим сериализатор с тем же набором атрибутов, что и класс модели Women:

class WomenSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=255)
    content = serializers.CharField()
    time_create = serializers.DateTimeField()
    time_update = serializers.DateTimeField()
    is_published = serializers.BooleanField(default=True)
    cat_id = serializers.IntegerField()

Обратите внимание, что здесь присутствует атрибут cat_id как поле с целочисленным значением, так как на вход сериализатора будет передаваться объект, где внешний ключ cat уже определен и сформирован как локальный атрибут cat_id, содержащий целое значение.

После этого переходим в файл women/views.py и в классе WomenAPIView применим этот сериализатор:

class WomenAPIView(APIView):
    def get(self, request):
        w = Women.objects.all()
        return Response({'posts': WomenSerializer(w, many=True).data})
 
 
    def post(self, request):
        post_new = Women.objects.create(
            title=request.data['title'],
            content=request.data['content'],
            cat_id=request.data['cat_id']
        )
 
        return Response({'post': WomenSerializer(post_new).data})

В методе get() мы теперь просто читаем все записи, представленные объектом QuerySet, и передаем эту коллекцию в сериализатор, дополнительно указываем параметр many=True, так как на выходе нужно сформировать список из записей таблицы (по умолчанию формируется одна запись). Далее, класс Response «знает» как обрабатывать объект сериализованных данных, превращая их в байтовую JSON-строку.

В методе post() происходит примерно то же самое, только без параметра many, так как возвращается всего одна запись.

Давайте посмотрим, как это будет работать. Запустим тестовый веб-сервер:

python manage.py runserver

Перейдем в программу Postman и выполним GET-запрос для адреса:

http://127.0.0.1:8000/api/v1/womenlist/

Как видим, возвращается JSON-строка со списком наших записей. Выполним теперь POST-запрос для того же адреса. И теперь возвратились данные, содержащие одну новую добавленную запись. Причем, мы здесь также видим значения времени, которое было автоматически подставлено нашим сериализатором.

Однако, если в POST-запросе передать неверные данные, то возникнет ошибка на этапе добавления записи в БД. Мы можем обработать этот момент с помощью нашего сериализатора, следующим образом:

    def post(self, request):
        serializer = WomenSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
 
        post_new = Women.objects.create(
            title=request.data['title'],
            content=request.data['content'],
            cat_id=request.data['cat_id']
        )
 
        return Response({'post': WomenSerializer(post_new).data})

Здесь мы формируем объект сериализации на основе принятых от клиента данных, а затем, вызываем метод is_valid() и указываем параметр raise_exception=True для генерации исключений (ошибок), которые будут передаваться в виде JSON-строки клиенту.

Давайте протестируем этот функционал. Снова передадим неверные данные (например, уберем title) и в ответе увидим:

{"title":["Обязательное поле."],"time_create":["Обязательное поле."],"time_update":["Обязательное поле."]}

По идее, поля time_create и time_update формируются автоматически и нам их передавать не нужно. Поэтому сообщения, что они обязательны здесь не совсем корректны. Они, конечно обязательны, но передавать клиенту их не нужно. Чтобы сериализатор корректно обрабатывал эти поля, в их определении нужно добавить аргумент read_only=True:

class WomenSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=255)
    content = serializers.CharField()
    time_create = serializers.DateTimeField(read_only=True)
    time_update = serializers.DateTimeField(read_only=True)
    is_published = serializers.BooleanField(default=True)
    cat_id = serializers.IntegerField()

Теперь, при отправке неверного запроса мы увидим, что требуется только одно поле title:

{"title":["Обязательное поле."]}

Давайте, ради интереса уберем параметр raise_exception=True и снова отправим те же данные. Как видите, появляется HTML-страница от фреймворка Django, сообщающая об ошибке при добавлении записи в БД. То есть, клиент получает уже не JSON-строку, а HTML-документ, что, в общем то, обычно считается неверным при реализации API. Поэтому всегда при валидации прописываем аргумент raise_exception=True.

Итак, на этом занятии мы с вами создали сериализатор на основе базового класса Serializer и научились его применять для кодирования и декодирования данных моделей.

Курс по Django: https://stepik.org/a/183363