やりたいこと
Django REST framework の ModelSerializer
では, ForeignKey
のフィールドは, シリアライズ/デシリアライズするときにネストを展開してしまいます.
例をあげますと,
titlefrom django.db import modelsimport uuidclass ForeignModel(models.Model):id = models.UUIDField(default=uuid.uuid4,primary_key=True,editable=False)name = models.CharField(max_length=255, unique=True)class SampleModel(models.Model):id = models.UUIDField(default=uuid.uuid4,primary_key=True,editable=False)foreign = models.ForeignKey(ForeignModel, on_delete=models.CASCADE)
のようにモデル定義されているとき, GET メソッドのレスポンスは
json{"id": "xxxxxx","foreign": {"id": "yyyyy","name": "myname"}}
このように展開されて, POST メソッドのパラメータは,
json{"foreign": {"name": "myname"}}
の形で指定する必要があります.
GET メソッドに関しては望ましいですが, POST メソッドのパラメータに関しては既存のオブジェクトのプライマリーキーが欲しい場合が多いと思います.
ということで,
- GET メソッドでは, インスタンスを JSON に展開する
- POST のパラメータでは, プライマリーキーのみを受け取る
という形でシリアライザを実装するのが主題です.
環境
環境は以下の通りです.
bash$ sw_versProductName: Mac OS XProductVersion: 10.15.6BuildVersion: 19G2021$ python -VPython 3.8.5$ pip listPackage Version------------------------- ---------Django 3.0.5djangorestframework 3.11.0drf-yasg 1.17.1packaging 20.4
解決策
シリアライザのフィールドには, write_only
や read_only
を指定ができるので, read_only
を指定した GET メソッド用のシリアライザフィールドと, write_only
を指定した POST メソッドのシリアライザフィールドを定義してあげることで, 期待する動作を実装できます.
GET メソッドを展開して受け取る
GET メソッドの要求は元々満たしていますが, 私は, API ドキュメントの自動生成ツールである drf_yasg を使っていて, 適切なビルドのため MethodSerializer
にて実装します.
titlefrom rest_framework import serializersfrom rest_framework.utils.serializer_helpers import ReturnDictfrom drf_yasg.utils import swagger_serializer_methodfrom typing import Dict, Anyfrom .models import ForeignModel, SampleModelclass ForeignSerializer(serializers.ModelSerializer):class Meta:model = ForeignModelfields = ('pk', 'name')class SampleSerializer(serializers.ModelSerializer):foreign = serializers.SerializerMethodField(read_only=True)class Meta:model = SampleModelfields = ('pk', 'foreign',)@swagger_serializer_method(serializer_or_field=ForeignSerializer)def get_foreign(self, instance: SampleModel) -> ReturnDict:return ForeignSerializer(instance.foreign).data
drf_yasg
を使っていないなら Meta.fields
に追加して, read_only
を指定してあげるだけで大丈夫なはずです.
POST パラメータには, プライマリーキーを渡す
POST メソッド用のフィールドには PrimaryKeyRelatedField
を使います.
write_only
にしつつ, シリアライズメソッドを上書きすることで対応します.
titlefrom rest_framework import serializersfrom rest_framework.utils.serializer_helpers import ReturnDictfrom drf_yasg.utils import swagger_serializer_methodfrom typing import Dict, Anyfrom .models import ForeignModel, SampleModelclass ForeignSerializer(serializers.ModelSerializer):class Meta:model = ForeignModelfields = ('pk', 'name')class SampleSerializer(serializers.ModelSerializer):foreign = serializers.SerializerMethodField(read_only=True)foreign_pk = serializers.PrimaryKeyRelatedField(queryset=ForeignModel.objects.all(),write_only=True)class Meta:model = SampleModelfields = ('pk', 'foreign', 'foreign_pk')@swagger_serializer_method(serializer_or_field=ForeignSerializer)def get_foreign(self, instance: SampleModel) -> ReturnDict:return ForeignSerializer(instance.foreign).datadef create(self, validated_data: Dict[str, Any]) -> SampleModel:foreign_pk = validated_data.get('foreign_pk', None)if foreign_pk is not None:validated_data['foreign'] = foreign_pkdel validated_data['foreign_pk']return super().create(validated_data)
これで,
- GET メソッド =>
foreign
にインスタンス情報が展開される - POST メソッド =>
foreign_pk
にプライマリーキーを渡す
という形になりました.
実際に API を叩いてみる
一応 API を叩いてみます. クライアントは, HTTPie を使っています.
bash$ http http://localhost:8080/samples/HTTP/1.1 200 OKAllow: GET, POST, HEAD, OPTIONSContent-Length: 249Content-Type: application/jsonDate: Thu, 01 Oct 2020 21:37:41 GMTServer: WSGIServer/0.2 CPython/3.8.5Vary: Accept, CookieX-Content-Type-Options: nosniffX-Frame-Options: DENY[{"foreign": {"name": "myname","pk": "2880c984-c284-4229-a1ca-c159e418511c"},"pk": "7d97ed86-55b5-4b5a-8e9e-bc26f2badc34"}]$ http POST http://localhost:8080/samples/ foreign_pk=2880c984-c284-4229-a1ca-c159e418511cHTTP/1.1 201 CreatedAllow: GET, POST, HEAD, OPTIONSContent-Length: 123Content-Type: application/jsonDate: Thu, 01 Oct 2020 21:40:07 GMTServer: WSGIServer/0.2 CPython/3.8.5Vary: Accept, CookieX-Content-Type-Options: nosniffX-Frame-Options: DENY{"foreign": {"name": "myname","pk": "2880c984-c284-4229-a1ca-c159e418511c"},"pk": "5889a2f0-0195-412b-b1f0-a4a8bd9efdb9"}
見ての通り, 期待通りの動作をしてくれているようです.