Типизация в drf-spectacular

gimntut

Гимаев Наиль

Posted on September 28, 2024

Типизация в drf-spectacular

Заранее прошу прощения, мне лень писать статью с скриншотами сваггера и с реальным кодом, поэтому пишу сразу без правок и вычиток. Если будет интерес, возможно, когда-нибудь оформлю в виде статьи на Хабре.

Этот пост - ответ на статью 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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Этот способ хорошо работает, даже если нужно вернуть список простых объектов, например List[str]

5. SerializerMethodField для объектов - это плохо 🟧

Со сложными объектами, не так всё просто. К примеру, у нас есть такой код

# 🟧
class ExperimentSerializer(DummySerializer):
    entity = SerializerMethodField()

    @staticmethod
    def get_entity(_) -> dict:
        return {"a": 1, "b": "2"}
Enter fullscreen mode Exit fullscreen mode

Как spectacular должен догадаться, как называются поля и какого типа у них значения? Без выполнения кода это не возможно.
Опытные разработчики могут догадаться, как ему подсказать

# ⁉
class EntityDict(TypedDict):
    a: int
    b: str


class ExperimentSerializer(Serializer):
    entity = SerializerMethodField()

    @staticmethod
    def get_entity(_) -> EntityDict:
        return {"a": 1, "b": "2"}
Enter fullscreen mode Exit fullscreen mode

Такое работает, в 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
Enter fullscreen mode Exit fullscreen mode

Работает, но тут есть нарушение DRY. Попробуем по другому:

# 🟧
class EntitySerializer(Serializer):
  a = IntegerField()
  b = CharField()

class ExperimentSerializer(Serializer):
    entity = SerializerMethodField()

    @staticmethod
    def get_entity(_) -> EntitySerializer:
        return EntitySerializer(obj.entity).data
Enter fullscreen mode Exit fullscreen mode

От дублирования избавились, 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
Enter fullscreen mode Exit fullscreen mode

Я мог бы написать ещё много о возможностях spectacular, но это как-нибудь в следующий раз.

💖 💪 🙅 🚩
gimntut
Гимаев Наиль

Posted on September 28, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related