JKになりたい

何か書きたいことを書きます。主にWeb方面の技術系記事が多いかも。

【Django REST framework】POST時はForeignKeyをpkのみ指定し、GET時はネストしたオブジェクトを展開する

前置き

以下が、今回の例で使用するViewとModelです。

class PostViewSet(viewsets.ModelViewSet):
    authentication_classes = [FirebaseAuthentication]
    queryset = Post.objects.all()
    serializer_class = PostSerializer
class Post(models.Model):
    user = models.ForeignKey(User)
    comment = models.CharField(max_length=130, default='')

class User(models.Model):
    uid = models.CharField(primary_key=True, max_length=64)
    name = models.CharField(max_length=30)
class PostSerializer(serializers.ModelSerializer):
    user = UserSerializer()

    class Meta:
        model = Post
        fields = ('id', 'user', 'comment')

このような実装でviewに対しGETでエンドポイントを叩くと、以下のようなレスポンスが帰ってきます。

[
    {
        "id": 1,
        "user": {
            "uid": "uid-1",
            "name": "hoge1"
        },
        "comment": "hoge",
    },
    {
        "id": 3,
        "user": {
            "uid": "uid-2",
            "name": "hoge1",
        },
        "comment": "hogehoge"
    }
]

完璧ですね。ネストしたフィールドであるuserが展開されています。

ただし、この状態だとPOSTするときは以下のようなjsonを投げないといけません。。

{
    "user": {
        "uid": "hoge",
        "name": "hoge"
    },
    "comment": "hoge"
}

これは思っているのと違います。userはuidを指定するようにしたいですね。

そこで、PostSerializerを以下のように変更してみます。

class PostSerializer(serializers.ModelSerializer):
    user = serializers.PrimaryKeyRelatedField(queryset=User.objects.all())

    class Meta:
        model = Post
        fields = ('id', 'user', 'comment')

これで、以下のようにuidでpostできるようになりました!

{
    "user": "hoge_uid",
    "comment": "",
}

ただし、GETが・・・

[
    {
        "id": 1,
        "user": "hoge_uid",
        "comment": "hoge"
    },
    {
        "id": 2,
        "user": "hoge_uid2",
        "comment": "hoge"
    }
]

userがuidしか帰ってこなくなってしまいました。。

これは思ってるのと違いますね。。両立する方法を模索してみます。

本題

ここから本題です。

先程の2つの要件「POST時はネストしたオブジェクトをpkで指定」「GET時はネストしたオブジェクトを展開」を満たすように実装をしていきます。

ModelとViewに変更はありません。Serializerだけ変えていきます。

まず、以下のようにuserのserializerをread onlyに指定します

class PostSerializer(serializers.ModelSerializer):
    user = UserSerializer(read_only=True)

これの状態だと、GET時はネストしたオブジェクトが展開されて返さられます。 が、POSTの際にuserの指定ができなくなってしまいます。

そこで、更にuid用のフィールドを追加します。

class PostSerializer(serializers.ModelSerializer):
    user = UserSerializer(read_only=True)
    user_uid = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), write_only=True)
    class Meta:
        model = Post
        fields = ('id', 'user', 'user_uid', 'comment')

このとき、write_onlyを指定することによりこのフィールドをGET時には出さないようにしておきます。

これで、GET時は展開され、POST時はpk(user_uid)を指定することが可能になりました。

ただし、この状態ではPOSTしたときに「user_uid」カラムがModelにないためエラーが吐かれます。 そこで、最後にcreateメソッドをオーバーライドします。

class PostSerializer(serializers.ModelSerializer):
    user = UserSerializer(read_only=True)
    user_uid = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), write_only=True)

    def create(self, validated_date):
        validated_date['user'] = validated_date.get('user_uid', None)

        if validated_date['user'] is None:
            raise serializers.ValidationError("user not found.") 

        del validated_date['user_uid']

        return Post.objects.create(**validated_date)

user_uidを指定すると、validated_date['user_uid']には指定されたpkの「Userオブジェクト」が入っています。

Modelのフィールド名はuser_uidでなくuserなので、そのようにマッピングを変更した上でPostオブジェクトをcreateしてあげればOKです。

これで、POST時は以下のようなフォーマットで、

{
    "user_uid": "hoge_uid",
    "comment": "hoge",
}

GET時は以下のように展開されて帰ってきます。

{
    "id": 1,
    "user": {
        "uid": "hoge_uid",
        "name": "hoge",
    },
    "comment": "hoge"
}

これで意図していた通りに実装ができました!

凄く簡単な要件ですが、少しだけ工夫が必要なんですねー。