DEV Community

Гимаев Наиль
Гимаев Наиль

Posted on

Типизация в 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, но это как-нибудь в следующий раз.

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay