В проекте Kittygram информация о котике содержит и год его рождения. Человеку, знакомому с календарём и арифметикой, не составит труда вычислить возраст котика. Но пользователи кошачьего API попросили разработчиков доработать сервис и явно возвращать возраст их любимцев в ответе на запрос.
Поле SerializerMethodField: обработка данных в сериализаторе
В уроке про проектирование API обсуждалось создание ответа, который содержит в числе прочего данные, которых нет в моделях. Задача с возрастом котика — тот самый случай. Давайте разберёмся, как это можно сделать, и поможет нам в этом SerializerMethodField.
SerializerMethodField — это поле для чтения, связанное с определённым методом, в котором будет вычислено значение этого поля. Метод, который будет вызван для поля <имя_поля>
, по умолчанию должен называться get_<имя_поля>
.
Аргументами метода get_<имя_поля>
должны быть self
и сериализуемый объект (он передаётся во второй аргумент):
def get_something(self, obj):
...
С помощью поля SerializerMethodField можно модифицировать существующее поле или создать новое.
Добавьте в сериализатор CatSerializer
поле age
(его нет в модели Cat); содержимое этого поля будет вычисляться «на лету» в методе get_age
:
...
import datetime as dt
...
class CatSerializer(serializers.ModelSerializer):
achievements = AchievementSerializer(many=True)
age = serializers.SerializerMethodField()
class Meta:
model = Cat
fields = ('id', 'name', 'color', 'birth_year', 'owner', 'achievements',
'age')
def get_age(self, obj):
return dt.datetime.now().year - obj.birth_year
В ответе на GET-запросы котика или котиков появится новое поле:
Хорошей практикой считается не перегружать этот метод какими-либо тяжёлыми операциями.
Пользовательский тип поля для сериализатора
Разработчики фронтенда сообщили, что по просьбе пользователей сайта они разрабатывают виджет-палитру для выбора цвета котиков.
Виджет будет передавать цвет на бэкенд через API в закодированном виде, в hex-формате, например — #ff0000
.
Коллеги попросили проверить, можно ли перенастроить бэкенд так, чтобы он мог работать с цветом в hex-формате. Отличная задача, приступим.
Сейчас в БД для каждого котика хранится название его цвета; название цвета — это обычная строка. Сериализатор для модели тоже ожидает цвет в строковом формате. Если в POST-запросе в поле color
вместо строки с названием цвета "красный"
придёт строка с кодом цвета "#ff0000"
, то код будет записан в БД. А нам это не надо, это не консистентно: изначально было принято решение хранить цвета в человекочитаемом виде; не будем нарушать это правило.
Цветовая модель RGB, которую применяют фронтендеры, может отобразить более 16 млн цветов, и, разумеется, не у всех цветов есть названия. Попытки поименовать все цвета RGB, конечно, делались — так появились палитры с цветами «В меру оливково-коричневый», «Коричневый цвета кожаного седла для лошади» и, разумеется, «Цвет бедра испуганной нимфы».
Но разработчики фронтенда клятвенно обещают, что их новый виджет будет возвращать только коды именованных цветов — другие они использовать не будут.
На сегодняшний день в спецификации цветов HTML и CSS определено и поименовано более сотни цветов.
Таким образом, задача сводится к тому, что необходимо создать такое поле для сериализатора, которое
- в режиме записи конвертирует код цвета в его название,
- в режиме чтения вернёт название цвета из БД.
Существует простая и удобная библиотека webcolors, которая позволяет конвертировать код цвета в его название и наоборот. Она нам пригодится: установите её в виртуальное окружение согласно инструкции.
Теперь разберёмся с полем сериализатора. Кроме стандартных, вложенных или related типов полей в сериализаторах можно описывать и применять собственные типы; это как раз то, что нужно.
Для создания собственного типа поля сериализатора нужно описать класс для нового типа, который будет унаследован от serializers.Field
и описать в нём два метода: def to_representation(self, value)
(для чтения) и def to_internal_value(self, data)
(для записи).
Опишем новый тип поля Hex2NameColor
в serializers.py:
import webcolors
...
class Hex2NameColor(serializers.Field):
# При чтении данных ничего не меняем - просто возвращаем как есть
def to_representation(self, value):
return value
# При записи код цвета конвертируется в его название
def to_internal_value(self, data):
# Доверяй, но проверяй
try:
# Если имя цвета существует, то конвертируем код в название
data = webcolors.hex_to_name(data)
except ValueError:
# Иначе возвращаем ошибку
raise serializers.ValidationError('Для этого цвета нет имени')
# Возвращаем данные в новом формате
return data
...
Теперь новый тип поля Hex2NameColor
можно присвоить полю color
в CatSerializer
:
...
class CatSerializer(serializers.ModelSerializer):
achievements = AchievementSerializer(many=True, required=False)
age = serializers.SerializerMethodField()
color = Hex2NameColor() # Вот он - наш собственный тип поля
class Meta:
model = Cat
fields = ('id', 'name', 'color', 'birth_year', 'owner', 'achievements',
'age')
...
Отправим POST-запрос на добавление нового котика, укажем в качестве цвета, например, код #228B22
и проверим результат:
Сработало! Строка с кодом конвертировалась, и теперь в БД цвет котика указан как forestgreen
. При этом существующие записи при GET-запросе возвращаются без изменений.
А вот если в POST-запросе передать код цвета, для которого не существует названия — сериализатор вернёт ошибку для этого поля:
Тестирование прошло удачно.
Над Москвой встаёт #287233
восход,
По мосту идёт #ffa500
кот,
И лоточник у метро продаёт
Апельсины цвета #f5f5dc
Когда разработчики фронтенда закончат работу над виджетом — можно будет без проблем настроить API для работы с новым форматом цвета. А пока вернём всё как было.
Переименование полей: параметр source
В модели Achievement есть поле name, но и в других моделях есть поля с таким именем. Разобраться можно, но в ответах это имя лучше поменять на более информативное achievement_name
.
Сериализатор, унаследованный от ModelSerializer
, по умолчанию использует те же названия полей, что и в модели, с которой он работает. Эти же имена служат ключами в ответе API.
Необходимость изменить имена возникает достаточно часто. Эту задачу решают через переопределение поля и применение параметра source:
new_field_name = serializers.CharField(source='old_field_name')
Определите новое поле achievement_name в сериализаторе, в качестве параметра передайте аргумент source=<'оригинальное имя поля в модели'>
.
class AchievementSerializer(serializers.ModelSerializer):
achievement_name = serializers.CharField(source='name')
class Meta:
model = Achievement
fields = ('id', 'achievement_name')
Теперь сериализатор знает, с каким полем из модели Achievement он должен работать, и соотнесёт всё правильно.
Этот подход работает при использовании сериализатора как для чтения, так и для записи.
Ограничение возможных значений поля: выбор из списка
Изменим возможности для выбора цвета котиков: набор кошачьих расцветок не особо велик, и можно заранее описать все доступные цвета. А потом можно будет организовать категоризацию по цветам.
Это можно сделать на уровне моделей:
from django.db import models
CHOICES = (
('Gray', 'Серый'),
('Black', 'Чёрный'),
('White', 'Белый'),
('Ginger', 'Рыжий'),
('Mixed', 'Смешанный'),
)
...
class Cat(models.Model):
name = models.CharField(max_length=16)
color = models.CharField(max_length=16, choices=CHOICES)
birth_year = models.IntegerField()
owner = models.ForeignKey(
Owner, related_name='cats', on_delete=models.CASCADE, blank=True,
null=True)
achievements = models.ManyToManyField(Achievement, through='AchievementCat')
def __str__(self):
return self.name
...
То же можно сделать и на уровне сериализатора, указав для поля color
тип ChoiceField
и передав в параметр choices
список с возможными вариантами:
...
from .models import Cat, Owner, Achievement, AchievementCat, CHOICES
...
class CatSerializer(serializers.ModelSerializer):
achievements = AchievementSerializer(many=True)
age = serializers.SerializerMethodField()
# Теперь поле примет только значение, упомянутое в списке CHOICES
color = serializers.ChoiceField(choices=CHOICES)
class Meta:
model = Cat
fields = ('id', 'name', 'color', 'birth_year', 'owner', 'achievements',
'age')
...
Изменили модели — не забыли создать и применить миграции. После этого можно проверять.
Теперь работа с цветом стандартизована; запрос, содержащий в поле color
название любого неучтённого в списке цвета, вызовет ошибку 400 Bad Request: