はじめに
こんにちは、サーバーサイドエンジニアの yusk です。
私はかねてからそこそこ Django
を好んで使っているのですが、その時に困って色々調べた上で難しかった、でも解決できたことのうちの一つである、**「SerializerMethodField
の機能のまま入力も受け付けたい」**という課題を解決したので、それについて記します。
Versions
- python 3.8
- django 3.1.6
- djangorestframework 3.12.2
SerializerMethodField とは
SerializerMethodField とは、公式によると以下のように説明されているものです。
This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object.
Google 翻訳様にかけると以下です
これは読み取り専用フィールドです。 アタッチされたシリアライザークラスのメソッドを呼び出して、その値を取得します。 オブジェクトのシリアル化された表現にあらゆる種類のデータを追加するために使用できます。
まずは、以下のコードを読んでみてください。
from rest_framework import serializers
from ..models import Task
class TaskSerializer(serializers.ModelSerializer):
parent_task_ids = serializers.SerializerMethodField()
class Meta:
model = Task
fields = ('id', 'title' 'parent_task_ids', )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['id'].read_only = True
def get_parent_task_ids(self, obj):
return [task.id for task in obj.parent_tasks.all()]
このような TaskSerializer
を書き ModelViewSet
でルーティングした場合、タスク作成の API を叩くときの request body は {"title": str}
となり、response は {"id": str, "title": str, "parent_task_ids": any}
となります。
便利ですね。
問題点
しかし、 この**parent_task_ids
の入力も受け付けられるようにしたい**となると、問題が生じます。
タスク作成の API を叩くときの request body が {"title": str, "parent_task_ids": any}
、response が {"id": str, "title": str, "parent_task_ids": any}
としたいとき、困難があります。
まず、SerializerMethodField
はread_only
なので、このままだと request body をとってくることができません。
では、以下のようにしてみてはどうでしょうか?
from rest_framework import serializers
from ..models import Task
class TaskSerializer(serializers.ModelSerializer):
parent_task_ids = serializers.CharField(required=False, help_text='e.g. "1,3"')
class Meta:
model = Task
fields = ('id', 'title' 'parent_task_ids', )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['id'].read_only = True
def get_parent_task_ids(self, obj):
return [task.id for task in obj.parent_tasks.all()]
こうした場合、一見うまくいきそうですが、うまくはいきません。
request body は、要望通り parent_task_ids
を受け付けることはできますが、response を生成するときに get_parent_task_ids
を呼ぶことはありません。
parent_task_ids_str
を write_only
で
CharField
に、parent_task_ids
を SerializerMethodField
にするとやりたいことはできます。
が、write 時と read 時で key の名前が変わってしまうのは気持ち悪い感じが残ります。
解決策
そこで私は、黒魔術であるメタプログラミングを用いて、上記課題を解決しました。
そのコードが以下です。
# utils.py
class WithMethodField:
def __init__(self, method_name=None, **kwargs):
self.method_name = method_name
super().__init__(**kwargs)
def bind(self, field_name, parent):
if self.method_name is None:
self.method_name = 'get_{field_name}'.format(field_name=field_name)
super().bind(field_name, parent)
def to_representation(self, value):
method = getattr(self.parent, self.method_name)
return method(value)
def get_attribute(self, instance):
return instance
def with_method_class(field_class):
return type(f"{field_class.__name__}WithMethodField", (
WithMethodField,
field_class,
), {})
from rest_framework import serializers
from ..models import Task
from .utils import with_method_class
class TaskSerializer(serializers.ModelSerializer):
parent_task_ids = with_method_class(serializers.CharField)(required=False, help_text='e.g. "1,3"')
class Meta:
model = Task
fields = ('id', 'title' 'parent_task_ids', )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['id'].read_only = True
def get_parent_task_ids(self, obj):
return [task.id for task in obj.parent_tasks.all()]
重要な行は、 parent_task_ids = with_method_class(serializers.CharField)(required=False, help_text='e.g. "1,3"')
の部分です。
この with_method_class
関数は、class を引数に取り、class を返します。
具体的には、WithMethodField
に引数にとった class を継承した class を返します。
この WithMethodField
class が SerializerMethodField
の必要な部分を持っています。
今回の例で言うと、 WithMethodField
と CharField
のそれぞれ必要な部分が合わさった class を生成しています。
これで、当初の目的であった、タスク作成の API を叩くときの request body が {"title": str, "parent_task_ids": any}
、response が {"id": str, "title": str, "parent_task_ids": any}
を実現することができました。
まとめ
with_method_class(serializers.CharField)()
とすることにより、CharField
と SerializerMethodField
のいいとこ取りをできる class を生成するメソッド、with_method_class
を自作しました。