DRFでシリアライザのForeignKeyフィールドをPOST時はプライマリーキーを渡し、GET時は展開する
10/1/2020 10:40:32 PM
Django
DRF
やりたいこと
Django REST framework のModelSerializer
では, ForeignKey
のフィールドは, シリアライズ/デシリアライズするときにネストを展開してしまいます.
例をあげますと,
title=models.pyfrom django.db import models import uuid class 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_vers ProductName: Mac OS X ProductVersion: 10.15.6 BuildVersion: 19G2021 $ python -V Python 3.8.5 $ pip list Package Version ------------------------- --------- Django 3.0.5 djangorestframework 3.11.0 drf-yasg 1.17.1 packaging 20.4
解決策
シリアライザのフィールドには,write_only
や read_only
を指定ができるので, read_only
を指定した GET メソッド用のシリアライザフィールドと, write_only
を指定した POST メソッドのシリアライザフィールドを定義してあげることで, 期待する動作を実装できます.
GET メソッドを展開して受け取る
GET メソッドの要求は元々満たしていますが, 私は, API ドキュメントの自動生成ツールである drf_yasg を使っていて, 適切なビルドのためMethodSerializer
にて実装します.
title=serializers.pyfrom rest_framework import serializers from rest_framework.utils.serializer_helpers import ReturnDict from drf_yasg.utils import swagger_serializer_method from typing import Dict, Any from .models import ForeignModel, SampleModel class ForeignSerializer(serializers.ModelSerializer): class Meta: model = ForeignModel fields = ('pk', 'name') class SampleSerializer(serializers.ModelSerializer): foreign = serializers.SerializerMethodField(read_only=True) class Meta: model = SampleModel fields = ('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
にしつつ, シリアライズメソッドを上書きすることで対応します.
title=serializers.pyfrom rest_framework import serializers from rest_framework.utils.serializer_helpers import ReturnDict from drf_yasg.utils import swagger_serializer_method from typing import Dict, Any from .models import ForeignModel, SampleModel class ForeignSerializer(serializers.ModelSerializer): class Meta: model = ForeignModel fields = ('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 = SampleModel fields = ('pk', 'foreign', 'foreign_pk') @swagger_serializer_method(serializer_or_field=ForeignSerializer) def get_foreign(self, instance: SampleModel) -> ReturnDict: return ForeignSerializer(instance.foreign).data def 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_pk del 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 OK Allow: GET, POST, HEAD, OPTIONS Content-Length: 249 Content-Type: application/json Date: Thu, 01 Oct 2020 21:37:41 GMT Server: WSGIServer/0.2 CPython/3.8.5 Vary: Accept, Cookie X-Content-Type-Options: nosniff X-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-c159e418511c HTTP/1.1 201 Created Allow: GET, POST, HEAD, OPTIONS Content-Length: 123 Content-Type: application/json Date: Thu, 01 Oct 2020 21:40:07 GMT Server: WSGIServer/0.2 CPython/3.8.5 Vary: Accept, Cookie X-Content-Type-Options: nosniff X-Frame-Options: DENY { "foreign": { "name": "myname", "pk": "2880c984-c284-4229-a1ca-c159e418511c" }, "pk": "5889a2f0-0195-412b-b1f0-a4a8bd9efdb9" }
見ての通り, 期待通りの動作をしてくれているようです.
<!-- ソースコードの全文は [d-kimuson/drf_foreign_serializer_sample](https://github.com/d-kimuson/drf_foreign_serializer_sample) に貼ってあります. -->