LoginSignup
4
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-23

はじめに

こんにちは、サーバーサイドエンジニアの 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} としたいとき、困難があります。

まず、SerializerMethodFieldread_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_strwrite_only
CharField に、parent_task_idsSerializerMethodField にするとやりたいことはできます。
が、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 の必要な部分を持っています。
今回の例で言うと、 WithMethodFieldCharField のそれぞれ必要な部分が合わさった class を生成しています。

これで、当初の目的であった、タスク作成の API を叩くときの request body が {"title": str, "parent_task_ids": any} 、response が {"id": str, "title": str, "parent_task_ids": any} を実現することができました。

まとめ

with_method_class(serializers.CharField)() とすることにより、CharFieldSerializerMethodField のいいとこ取りをできる class を生成するメソッド、with_method_class を自作しました。

参考

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1