Типизация в drf-spectacular
Гимаев Наиль
Posted on September 28, 2024
Заранее прошу прощения, мне лень писать статью с скриншотами сваггера и с реальным кодом, поэтому пишу сразу без правок и вычиток. Если будет интерес, возможно, когда-нибудь оформлю в виде статьи на Хабре.
Этот пост - ответ на статью https://habr.com/ru/companies/amvera/articles/843232/
Статья хороша. Вот только есть одно очень большое НО, и это - декораторы: extend_schema и т.п.
1. Декораторы - это плохо 🟧
drf-spectacular
- это уже третья библиотека для swagger в проекте, который я веду. Сначала был django-rest-swagger - в нём документация велась с помощь doc-string
. В какой-то момент эта библиотека передала эстафету библиотеке drf-yasg в ней уже были декораторы. Счастье было долгим, но нужна была поддержка OpenAPI v3. Всем желающим drf-yasg
предложил перейти на drf-spectacular.
Мне дважды пришлось переписывать документацию к swagger и мне это не понравилось, т.к. проект большой.
К счастью, drf-spectacular
(далее spectacular) спроектирован так, что если API написан правильно, то декораторы вообще не нужны.
2. GenericAPIView
и GenericViewSet
- это хорошо ✅
Достаточно унаследовать свои вьюхи от указанных классов и spectacular сам извлечёт необходимые сведения из get_queryset
и get_serializer_class
.
Т.е. можно написать такой код и swagger
сформирует правильную документацию
def get_serializer_class(self):
if self.action == "create":
return CreateSerializer
if self.action == "retrive":
return RetriveSerializer
return super().get_serializer_class()
3. Иногда декораторы - это необходимость ✅
Возникает вопрос, зачем нужны декораторы, если spectacular
справляется сам. Иногда нужно писать API, которое берёт данные не из БД. Разного рода API-калькуляторы, или API-посредники, которые возвращают вычисленные данные или полученные из вне. В этом случае, приходится наследовать вьюху от APIView
. Тут без декоратора не обойтись.
4. SerializerMethodField
с простыми типами - это хорошо ✅
Если поля имеют простые типы: IntergerField
, CharField
, то spectacular справляется очень хорошо. Но если используется SerializerMethodField
, то ему уже нужны подсказки. Для простых типов это просто, достаточно указать тип возвращаемой функции.
class MySerializer(Serializer):
a = IntegerField()
b = IntegerField()
sum = SerializerMethodField()
def get_sum(self, obj) -> int: # Возвращаемый тип: int
return obj.a + obj.b
Этот способ хорошо работает, даже если нужно вернуть список простых объектов, например List[str]
5. SerializerMethodField для объектов - это плохо 🟧
Со сложными объектами, не так всё просто. К примеру, у нас есть такой код
# 🟧
class ExperimentSerializer(DummySerializer):
entity = SerializerMethodField()
@staticmethod
def get_entity(_) -> dict:
return {"a": 1, "b": "2"}
Как spectacular должен догадаться, как называются поля и какого типа у них значения? Без выполнения кода это не возможно.
Опытные разработчики могут догадаться, как ему подсказать
# ⁉
class EntityDict(TypedDict):
a: int
b: str
class ExperimentSerializer(Serializer):
entity = SerializerMethodField()
@staticmethod
def get_entity(_) -> EntityDict:
return {"a": 1, "b": "2"}
Такое работает, в swagger появится правильное описание. Но иногда нужно получить данные из другого сериализатора:
# 🟧
class EntityDict(TypedDict):
a: int
b: str
class EntitySerializer(Serializer):
a = IntegerField()
b = CharField()
class ExperimentSerializer(Serializer):
entity = SerializerMethodField()
@staticmethod
def get_entity(_) -> EntityDict:
return EntitySerializer(obj.entity).data
Работает, но тут есть нарушение DRY. Попробуем по другому:
# 🟧
class EntitySerializer(Serializer):
a = IntegerField()
b = CharField()
class ExperimentSerializer(Serializer):
entity = SerializerMethodField()
@staticmethod
def get_entity(_) -> EntitySerializer:
return EntitySerializer(obj.entity).data
От дублирования избавились, swagger всё ещё работает, но тип метода get_entity
не соответствует возвращаемым данным.
В общем, для своего проекта я написал класс DataSerializerField, который решает эту проблему.
Пример использования:
# ✅
class EntitySerializer(Serializer):
a = IntegerField()
b = CharField()
class ExperimentSerializer(Serializer):
entity = DataSerializerField(EntitySerializer)
@staticmethod
def get_entity_data(_):
return obj.entity
Я мог бы написать ещё много о возможностях spectacular, но это как-нибудь в следующий раз.
Posted on September 28, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.