【DRF】Django Rest Framework で入力を受け付けられる SerializerMethodField を使いたい

## はじめに

こんにちは、サーバーサイドエンジニアの 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 翻訳様にかけると以下です

> これは読み取り専用フィールドです。 アタッチされたシリアライザークラスのメソッドを呼び出して、その値を取得します。 オブジェクトのシリアル化された表現にあらゆる種類のデータを追加するために使用できます。


まずは、以下のコードを読んでみてください。

```python
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 をとってくることができません。

では、以下のようにしてみてはどうでしょうか？

```python
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 の名前が変わってしまうのは気持ち悪い感じが残ります。

## 解決策

そこで私は、黒魔術であるメタプログラミングを用いて、上記課題を解決しました。
そのコードが以下です。

```python
# 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,
    ), {})
```

```python
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` を自作しました。

## 参考

* https://www.django-rest-framework.org/api-guide/fields/#serializermethodfield
* https://github.com/encode/django-rest-framework/blob/master/rest_framework/fields.py#L1856