はじめに
こんにちは、サーバーサイドエンジニアの 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 を自作しました。