atama plus techblog
🤹

drf-spectacularでDRFのGenericForeignKeyをOpenAPIスキーマに反映する

2024/12/03に公開

はじめに

こんにちは! atama plusでWebエンジニアをしているnaoshiです。
この記事はatama plus Advent Calendar 2024の2日目の記事です。

弊社では、Django REST Framework(DRF)をバックエンドに採用しており、drf-spectacular を用いてOpenAPIスキーマを生成しています。

APIスキーマを生成する際に、GenericForeignKeyを持つフィールドがスキーマに反映されないという状況がありました。
あまり遭遇することのない状況かもしれませんが、本記事では、その対処法を簡単にご紹介します。

課題:GenericForeignKey を持つフィールドがスキーマに反映されない

次のような Comment モデルを例に説明します。
created_by フィールドには Teacher または Manager を関連付けられるよう、Djangoの GenericForeignKey を使用しています。
(参考:Django Generic relations

class Comment(models.Model):
  comment = models.CharField(max_length=255)
  created_by_content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
  created_by_object_id = models.PositiveIntegerField()
  created_by = GenericForeignKey('created_by_content_type', 'created_by_object_id')
  created_at = models.DateTimeField(auto_now_add=True)

さらに、シリアライズ用に以下のカスタムフィールドを定義しました。
(参考:DRF Generic relationships

class CommentCreatedByField(serializers.RelatedField):
  def to_representation(self, value):
    if isinstance(value, Teacher):
      return TeacherSerializer(obj.created_by).data
    elif isinstance(value, Manager):
      return ManagerSerializer(obj.created_by).data
    else:
      raise Exception('Unexpected type of commenter')

class CommentSerializer(serializers.ModelSerializer):
  created_by = CommentCreatedByField(read_only=True)

  class Meta:
    model = Comment
    fields = ['comment', 'created_by', 'created_at']

ここで、drf-spectacularでスキーマを生成すると、created_byフィールドが反映されません。

Comment:
  type: object
  properties:
    id:
      type: integer
    comment:
      type: string
    created_at:
      type: number
      readOnly: true

解決方法:PolymorphicProxySerializer と extend_schema_field を使う

この課題を解決するために、PolymorphicProxySerializerとextend_schema_fieldを使いました。

以下は変更後のシリアライザです。

comment_created_by_serializer = PolymorphicProxySerializer(
    component_name="CommentCreatedBy",
    serializers={
        "Teacher": TeacherSerializer,
        "Manager": ManagerSerializer,
    },
    resource_type_field_name=None,
)

@extend_schema_field(comment_created_by_serializer)
class CommentCreatedByField(serializers.RelatedField):
  def to_representation(self, value):
    if isinstance(value, Teacher):
      return TeacherSerializer(obj.created_by).data
    elif isinstance(value, Manager):
      return ManagerSerializer(obj.created_by).data
    else:
      raise Exception('Unexpected type of commenter')
    return None

class CommentSerializer(serializers.ModelSerializer):
  created_by = CommentCreatedByField(read_only=True)

  class Meta:
    model = Comment
    fields = ['comment', 'created_by', 'created_at']

結果

修正後、スキーマは次のように生成され、created_byフィールドが正確に記述されるようになりました。

Comment:
  type: object
  properties:
    id:
      type: integer
    comment:
      type: string
    created_by:
      allOf:
        - $ref: "#/components/schemas/CommentCreatedBy"
      readOnly: true
    created_at:
      type: number
      readOnly: true
CommentCreatedBy:
  oneOf:
    - $ref: "#/components/schemas/Teacher"
    - $ref: "#/components/schemas/Manager"

まとめ

今回は、PolymorphicProxySerializerとextend_schema_fieldを使ってGenericForeignKeyを持つフィールドをスキーマに反映する方法を紹介しました。

少しでも参考になれば幸いです!

atama plus techblog
atama plus techblog

Discussion