Django

Django REST Frameworkでfieldの指定をする方法

公開日:2023-10-15 更新日:2023-10-15

最近Django REST Frameworkを触っています。

そういえば「fieldを指定したGETリクエストってどうやってやるんだっけ?」と気になったので調べてみました。

モデル

例によってブログです。

以下のようなシンプルな構成でテストしていきます。

from django.db import models
from django.utils import timezone


class Post(models.Model):
    """記事"""
    title = models.CharField('タイトル', max_length=40)
    description = models.TextField('紹介文')
    main_text = models.TextField('本文')
    created_at = models.DateTimeField('作成日', default=timezone.now)

    def __str__(self):
        return self.title

タイトルと詳細と本文と作成日という非常にシンプルな構成です。

ビューとシリアライザー

続いてビューとシリアライザーをざっくりと書いていきます。

# serializers.py
from rest_framework import serializers
from .models import Post


class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = '__all__'

あとはviews.pyでこのシリアライザーを指定します。

# views.py
from rest_framework import generics
from .models import Post
from .serializers import PostSerializer


class PostList(generics.ListAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

簡易的ですが、記事一覧の取得APIができました。

リクエストを送ってみる

開発サーバーを起動させてリクエストを送ってみましょう。

リクエストURLはhttp://127.0.0.1:8000/api/blog/posts/としました。

方法は何でもいいんですが、requestsモジュールを使いました。

import requests

url = 'http://127.0.0.1:8000/api/blog/posts/'

r = requests.request('GET', url)
from pprint import pprint

pprint(r.json())

以下のように全てのフィールドが取得されます。

[{'created_at': '2022-05-15T08:17:01+09:00',
  'description': 'test',
  'id': 1,
  'main_text': 'test',
  'title': 'test'},
 {'created_at': '2023-10-15T20:03:59+09:00',
  'description': '柴田聡子の新曲について',
  'id': 3,
  'main_text': 'Synergyは最高だ。',
  'title': '柴田聡子について'},
 {'created_at': '2023-10-15T20:17:51+09:00',
  'description': '柴田聡子の名曲、ラッキーカラーについての記事です',
  'id': 4,
  'main_text': 'たんたんとした愛がわからなくなってすぐに地図を見てる、という歌詞が印象に残った',
  'title': 'ラッキーカラーについて'}]

フィールドを指定できるようにする

ここからフィールドを指定できるようにしていきます。

例えばmain_textなどはブログの本文が入ることから、一覧ページでは取得の必要がない、といった要件はあると思います。

一つの方法としてはmain_textを除いた一覧用のシリアライザを作成するなどがあります。

今回は、リクエストをする際に動的に指定をできるようにしていきます。

まず以下のようにフィールドを動的に管理できるサブクラスシリアライザを作成します。

from rest_framework import serializers
from .models import Post


class DynamicFieldsModelSerializer(serializers.ModelSerializer):
    """
    モデルシリアライザーのサブクラス
    フィールドを動的に管理する
    """

    def __init__(self, *args, **kwargs):
        # インスタンス化時に 指定された'fields'の値を取得する
        fields = kwargs.pop('fields', None)
        super().__init__(*args, **kwargs)

        if fields:
            # 指定されていないfieldsをとりのぞく
            allowed = set(fields)
            existing = set(self.fields)
            for field_name in existing:
                # 指定されていない
                if field_name not in allowed:
                    # とりのぞく
                    self.fields.pop(field_name)


# DynamicFieldsModelSerializerを継承
class PostSerializer(DynamicFieldsModelSerializer):
    class Meta:
        model = Post
        fields = '__all__'

次にビュー側でリクエストがあった際に、指定されたfieldsの値をリスト化して、シリアライザをインスタンス化する…という風にします。

from rest_framework import generics
from .models import Post
from .serializers import PostSerializer


class PostList(generics.ListAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

    def get_serializer(self, *args, **kwargs):
        """
        このビューで使用されるシリアライザーのインスタンスを返す
        """
        serializer_class = self.get_serializer_class()
        kwargs['context'] = self.get_serializer_context()

        fields = self.request.query_params.get('fields')
        if fields:
            # リスト化してシリアライザに渡す
            fields = fields.split(',')
            kwargs['fields'] = fields

        return serializer_class(*args, **kwargs)

呼び出すときは以下のように辞書形式でparamsを渡します。

import requests

url = 'http://127.0.0.1:8000/api/blog/posts/'

# idとtitleフィールドを指定
params = {'fields': 'id,title'}
r = requests.request('GET', url, params=params)
from pprint import pprint

pprint(r.json())
[{'id': 1, 'title': 'test'},
 {'id': 3, 'title': '柴田聡子について'},
 {'id': 4, 'title': 'ラッキーカラーについて'}]

期待している通りにidとtitleだけ取り出せました。

流れを振り返ると、クライアントはクエリパラメータfieldsを使って、レスポンスに含めてほしいフィールド名をカンマ区切りで指定します。

リクエストがDjangoアプリケーションのサーバーに到達すると、PostListビューがこれを処理します。

ビューのget_serializerメソッドが、リクエストからfieldsクエリパラメータを取得し、リストに変換します。

このリストは、後続の処理でシリアライザーに渡されるfields引数となります。

PostSerializer(DynamicFieldsModelSerializerを継承)がfields引数を受け取り、この引数に基づいてインスタンスのフィールドセットを動的に調整します。具体的には、fieldsに含まれない全てのフィールドを削除する処理をしています。

そうしてシリアライザが生成したデータがHTTPレスポンスとして、最終的にクライアントに送り返されました。

フィールドを取り除いた指定をできるようにする

おまけで個別に指定するだけではなくて、特定のフィールド以外…という指定もできるようにしてみます。

class DynamicFieldsModelSerializer(serializers.ModelSerializer):
    """
    モデルシリアライザーのサブクラス
    フィールドを動的に管理する
    """

    def __init__(self, *args, **kwargs):
        fields = kwargs.pop('fields', None)
        # 追加、同様に取得する
        exclude = kwargs.pop('exclude', None)
        super().__init__(*args, **kwargs)

        if fields:
            allowed = set(fields)
            existing = set(self.fields)
            for field_name in existing:
                if field_name not in allowed:
                    self.fields.pop(field_name)
        # 指定されたfieldを取り除く        
        if exclude:
            for field_name in exclude:
                self.fields.pop(field_name, None)
        

ビュー側にも同様に追加。

class PostList(generics.ListAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

    def get_serializer(self, *args, **kwargs):
        """
        このビューで使用されるシリアライザーのインスタンスを返す
        """
        serializer_class = self.get_serializer_class()
        kwargs['context'] = self.get_serializer_context()

        fields = self.request.query_params.get('fields')
        # 追加
        exclude = self.request.query_params.get('exclude')
        if fields:
            fields = fields.split(',')
            kwargs['fields'] = fields
        # 追加
        if exclude:
            exclude = exclude.split(',')
            kwargs['exclude'] = exclude

        return serializer_class(*args, **kwargs)

リクエストを送ってみましょう。

import requests

url = 'http://127.0.0.1:8000/api/blog/posts/'

# main_text以外を指定
params = {'exclude': 'main_text'}
r = requests.request('GET', url, params=params)
from pprint import pprint

pprint(r.json())
[{'created_at': '2022-05-15T08:17:01+09:00',
  'description': 'test',
  'id': 1,
  'title': 'test'},
 {'created_at': '2023-10-15T20:03:59+09:00',
  'description': '柴田聡子の新曲について',
  'id': 3,
  'title': '柴田聡子について'},
 {'created_at': '2023-10-15T20:17:51+09:00',
  'description': '柴田聡子の名曲、ラッキーカラーについての記事です',
  'id': 4,
  'title': 'ラッキーカラーについて'}]

うまくmain_text以外のfieldを取得することができました。

Twitter Share