DRFでシリアライザのForeignKeyフィールドをPOST時はプライマリーキーを渡し、GET時は展開する

10/1/2020 10:40:32 PM

Django
DRF

やりたいこと

Django REST framework の ModelSerializer では, ForeignKey のフィールドは, シリアライズ/デシリアライズするときにネストを展開してしまいます.

例をあげますと,

title=models.py
from 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_onlyread_only を指定ができるので, read_only を指定した GET メソッド用のシリアライザフィールドと, write_only を指定した POST メソッドのシリアライザフィールドを定義してあげることで, 期待する動作を実装できます.

GET メソッドを展開して受け取る

GET メソッドの要求は元々満たしていますが, 私は, API ドキュメントの自動生成ツールである drf_yasg を使っていて, 適切なビルドのため MethodSerializer にて実装します.
title=serializers.py
from 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.py
from 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) に貼ってあります. -->