drf-spectacularでDRFのGenericForeignKeyをOpenAPIスキーマに反映する
はじめに
こんにちは! 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を使いました。
-
PolymorphicProxySerializer
特定のフィールドが複数の異なるシリアライザ(または型)を取りうる場合に、それを包括的に表現できる。
(参考:drf_spectacular PolymorphicProxySerializer) -
extend_schema_field
フィールドに対して明示的にスキーマを指定できる。
(参考:drf_spectacular 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を持つフィールドをスキーマに反映する方法を紹介しました。
少しでも参考になれば幸いです!
Discussion