django-rest-framework-json-api のバージョンを 2.6.0 にあげると includeしていたserializerのattributes が全てnullになった話

自分が今勤めている会社 オミカレでは APIの環境を Django を採用して開発をしています。

employment.en-japan.com

特に Django では Django Rest Framework と Django Rest Framework JSON API というライブラリを導入していて、Djangoを初めて触り始めた 2年前ぐらいから、改めてDjangoのフルスタック感というのを感じていました。

www.django-rest-framework.org

github.com

先日、弊社の 改善Day という仕組みの中で Libraryのバージョンアップを行おうと急に思いたち、改善をやってみる事があったのですが、そこで落とし穴を踏んだので共有をさせて頂きたいと思います。

今回の説明の為の例のAPI共有

※ データベースの情報などは例です

  • /api/user/me というAPIにて 自分のuser の情報 を返すAPI を想定します。
  • バージョンアップ前は JSON API に則って、以下のようなレスポンスが返ってきていました。
    • user と pref テーブルが紐付いている、みたいな実装です。
{
    "data": {
        "type": "user",
        "id": "1",
        "attributes": {
            "name": "sample",
            "created_at": "2020-10-16T17:15:41.709600+09:00",
            "updated_at": "2020-10-16T17:15:41.709600+09:00"
        },
        "relationships": {
            "pref": {
                "data": {
                    "type": "pref",
                    "id": "1"
                }
            }
        }
    },
    "included": [
        {
            "type": "pref",
            "id": "1",
            "attributes": {
                "name": "北海道"
            }
        }
    ]
}

API側の実装

今回対象となった実装はこんな感じでした。

class PrefSerializer(serializers.ModelSerializer):
    class Meta:
        model = Pref
        fields = ('id', 'name', )

    class JSONAPIMeta:
        resource_name = 'pref'

class UserSerializer(serializers.ModelSerializer):
    pref = PrefSerializer()

    class Meta:
        model = User
        fields = ('id', 'name', 'created_at', 'updated_at', 'pref', )

    class JSONAPIMeta:
        resource_name = 'user'
        included_resources = ['pref']

バージョンアップ後何が起きたか

  • 上記の実装の前提で バージョンを上げると、以下のdiff のようにレスポンスが変わりました。
    • included の attributes の中身が全て null になるような感じです。
{
    "data": {
        "type": "user",
        "id": "1",
        "attributes": {
            "name": "sample",
            "created_at": "2020-10-16T17:15:41.709600+09:00",
            "updated_at": "2020-10-16T17:15:41.709600+09:00"
        },
        "relationships": {
            "pref": {
                "data": {
                    "type": "pref",
                    "id": "1"
                }
            }
        }
    },
    "included": [
        {
            "type": "pref",
            "id": "1",
            "attributes": {
-                 "name": "北海道"
+                 "name": null
            }
        }
    ]
}

原因と対策

原因

今回、私がハマった事象が既に issue にも上がっていました。

github.com

I think I did find where this stopped working when using the ModelSerializer as the related field - https://github.com/django-json-api/django-rest-framework-json-api/commit/22c4587c98cd4f05c0a4378fd6f5627ab420c532 .

2.6.0 の中で field ( 今回でいうと、 UserSerializer の Meta class の中で定義されているもの )について instance が ModelSerializer を継承した objectであった場合に attributes を None (null) で返すという実装が入っておりました。

実際に例では PrefSerializer は ModelSerializer を継承しています。 この為、この実装では attributes は null になってしまうようです。

対策

実際の仕様例としては公式ドキュメントに沿って以下のように実装すべきかと思います。

django-rest-framework-json-api.readthedocs.io

class PrefSerializer(serializers.ModelSerializer):
    class Meta:
        model = Pref
        fields = ('id', 'name', )

    class JSONAPIMeta:
        resource_name = 'pref'

class UserSerializer(serializers.ModelSerializer):
-     pref = PrefSerializer()
+     included_serializers = {
+         'knight': PrefSerializer
+     }

    class Meta:
        model = User
        fields = ('id', 'name', 'created_at', 'updated_at', 'pref', )

    class JSONAPIMeta:
        resource_name = 'user'
        included_resources = ['pref']

上記のように実装する事で正常に attributes の中に辺りが入る事も確認出来ました。

ModelSerializer を埋め込む実装は非推奨だったのか

先程の issue にもあるように Django Rest Framework の方には ModelSerializer ではなくて 普通の Serializer を継承したもので Object を ネストする場合の手法として以下のように紹介されています。

class UserSerializer(serializers.Serializer):
    email = serializers.EmailField()
    username = serializers.CharField(max_length=100)

class CommentSerializer(serializers.Serializer):
    user = UserSerializer()
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

by. https://www.django-rest-framework.org/api-guide/serializers/#dealing-with-nested-objects

また、ModelSerializer は Serializer class の内、 fields を Django の Modelクラスから生成する ショートカットを提供すると書かれてるので、そこまで非推奨では無いように思ったりします。

The ModelSerializer class provides a shortcut that lets you automatically create a Serializer class with fields that correspond to the Model fields.

by. https://www.django-rest-framework.org/api-guide/serializers/#modelserializer

...とは思いましたが、 2.6.0 のリリースから 2年弱 ... 3.2.0 のバージョンで正式な 非推奨アナウンスが出ていました。

Deprecated
・ Rendering nested serializers as relationships is deprecated. Use ResourceRelatedField instead

by. https://github.com/django-json-api/django-rest-framework-json-api/blob/master/CHANGELOG.md#deprecated

この書き方からすると 以下のも非推奨になるんだろうか。

class UserSerializer(serializers.Serializer):
    email = serializers.EmailField()
    username = serializers.CharField(max_length=100)

class CommentSerializer(serializers.Serializer):
    user = UserSerializer()
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

ちなみに 3.2.0 のリリースノートから抜粋すると 以下のオプションがサポートされており、これを有効にする事で今の ModelSerializerを実装する方針であっても延命する措置が提供されていたりします。

Added
・ Added support for serializing nested serializers as attribute json value introducing setting JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE

3.2.0 まであげると、この問題は一時的には解決するかもしれませんが、django-rest-framework-json-api ライブラリは 3.0.0 で 後方互換を破壊する変更が入ってるみたいなのでその辺りはご確認頂いた方が良さそうです。

結論

という事で実装について諸々学びがありました。

ドキュメントを読む重要さを予め実感出来た気がしますね〜。