Serializer
何をするもの?
データに対してシリアライズとデシリアライズを行うところ。
デシリアライズとは?
入力データを元になんらかのオブジェクトを作成すること。
まずは基本の基本からやるとして、こういうモデルがあるとする。
class Book(object):
def __init__(self, title, author, price, created=None):
self.title = title
self.author = author
self.price = price
self.created = created or datetime.now()
このモデルに対してこういうシリアライザを作ったとする。
class BookSerializer(serializers.Serializer):
title = serializers.CharField(max_length=255)
author = serializers.CharField(max_length=255)
price = serializers.IntegerField()
created = serializers.DateTimeField()
def create(self, validated_data):
return Book(**validated_data)
def update(self, instance, validated_data):
instance.title = validated_data.get('title', instance.title)
instance.author = validated_data.get('author', instance.author)
instance.price = validated_data.get('price', instance.price)
instance.created = validated_data.get('created', instance.created)
return instance
これに対して以下のようにデータを入れてみる。
serializer = BookSerializer(data={'title': 'Test Option Book', 'author': 'testuser', 'price': 800, 'created': '2016-01-27T15:17:10.375877Z'})
ここで見るのは BookSerializer の引数。
serializer のメソッドを見ると第 1 引数と第 2 引数のあるなしがわかる。
serializer を呼び出した時に指定した引数によってこのメソッドの呼び出しが変わるということになる。
今回は第 1 引数のみなので create()
が呼び出されることになる。
あとは
>>> serializer.is_valid()
True
>>> serializer.save()
<books.views.Book object at 0x00000215AFDBDD60>
>>> serializer.validated_data
OrderedDict([('title', 'Test Option Book'), ('author', 'testuser'), ('price', 800), ('created', datetime.datetime(2016, 1, 27, 15, 17, 10, 375877, tzinfo=backports.zoneinfo.ZoneInfo(key='UTC')))])
>>> book.price
800
という流れになる。
当然 update()
を呼び出すときは以下のように data 引数の値は一部のみの指定だけでも問題ない。
>>> serializer = BookSerializer(book, data={'price': 600}, partial=True)
>>> serializer.is_valid()
True
>>> serializer.save()
<books.views.Book object at 0x00000215AFDBDD60>
>>> book.price
600
>>>
# serializer.is_valid()後はsaveのみでもよい
>>> serializer.save(price=1000)
<books.views.Book object at 0x00000215AFDBDD60>
>>> book.price
1000
ここまでをまとめると
デシリアライズは入力されたデータを引数としてなんらかのオブジェクトを作成することを指す(入力されるデータは QuerySet など Dict に準ずる)
serializer は引数によってどのメソッドが呼びされるか決まる(つまり、serializer のメソッドはここを逆算して何を引数とするのか、何をやるのか定義することになる)
serializer 呼び出し → serializer.is_valid()
→ serializer.save()
でワンセット。特にis_valid()
が実行されない限りはsave()
もできず、validated_data
も参照できない。
デシリアライズされたデータを参照したい場合はserializer.validated_data
を参照する。
シリアライズとは
オブジェクトを元にデータを読み出すこと。または、そのオブジェクトを整形する処理。
# インスタンス(オブジェクト)作成
book = Book(title='Test Serialize', author='testuser1', price='2000')
# デシリアライズ
>>> serializer = BookSerializer(book)
# 読み出す
>>> serializer.data
{'title': 'Test Serialize', 'author': 'testuser1', 'price': 2000, 'created': '2022-02-21T01:17:04.129001Z'}
>>>
次に API の流れを見てみる。
新規登録(デシリアライズ)
- フロントから RequestがJSONで送られParserに通される
- Request の data 属性の値をシリアライザの引数に渡し、シリアライザのインスタンス化を行う(ex.
serializer = BookSerializer(data={'price': 600}, partial=True)
) - is_valid()実行。_validated_data が作成され、以後参照できるようになる。
- save()メソッドで_validated_data を元にオブジェクトが作成される。ModelSerializer 継承のインスタンスならここでデータベースにモデルオブジェクトととして登録される。
ただし、read_only=True(読み出し専用、書き出し不可)のフィールドに対してはこの処理は行えない(dataにそれが混ざってたらエラー)
更新(デシリアライズ)
- フロントから Request
- Request の data 属性の値及び更新対象のモデルオブジェクトを渡し、シリアライザのインスタンス化を行う(ex.
serializer = BookSerializer(book, data={'price': 600}, partial=True)
) - is_valid()実行。_validated_data が作成され、以後参照できるようになる。
- save()メソッドで_validated_data を元にオブジェクトが作成される。このオブジェクトで対象のモデルオブジェクトの更新を行う
ただし、read_only=True(読み出し専用、書き出し不可)のフィールドに対してはこの処理は行えない(dataにそれが混ざってたらエラー)
出力(シリアライズ)
- シリアライザからシリアライザオブジェクトを作成する
- Renderがシリアライザオブジェクトのdata属性へアクセスする
- 出力用のフィールドが抽出されたdictのオブジェクトが取り出され、Renderを通してJSONでResponseでフロントへ返る
ここはちょっと分かりづらいのでshellで1~3の流れを実行してみる。
>>> from rest_framework import serializers
>>> from api.views import Book
>>> from rest_framework.renderers import JSONRenderer
>>> from api.serializers import BookSerializer
# シリアライズインスタンス用意(本来はBook.objects.getなどでモデルからObjectを取得して引数に入れる)
>>> serializer = BookSerializer(data={'title': 'Test Option Book', 'author': 'testuser', 'price': 800, 'created': '2016-01-27T15:17:10.375877Z'})
>>> serializer.is_valid()
True
>>> serializer.save()
<api.views.Book object at 0x00000238799D3BE0>
# 出力用データ、これにRenderがアクセスする。ただし、write_only=True(読み出し不可で書き出し専用)のフィールドは不可能
>>> serializer.data
{'title': 'Test Option Book', 'author': 'testuser', 'price': 800, 'created': '2016-01-27T15:17:10.375877Z'}
# Renderによってjsonに
>>> JSONRenderer().render(serializer.data)
b'{"title":"Test Option Book","author":"testuser","price":800,"created":"2016-01-27T15:17:10.375877Z"}'
ModelSerializerでの例
引用: くろのてさん より [Django REST Framework] Serializer の 使い方 をまとめてみた
>>> import uuid
>>> from django.contrib.auth.models import User
>>> from django.contrib.auth.hashers import make_password
>>> from rest_framework import serializers
>>> from api.serializers import UserSerializer
>>> serializer = UserSerializer(data={'first_name': 'Takanori', 'last_name': 'Shimizukawa', 'username': 'test', 'email': 'test@example.com'})
>>> serializer.is_valid()
True
>>> U = serializer.save()
>>> serializer.data
{'username': '2e6587340c5a41ada86b2e50d1ac2c08', 'email': 'test@example.com', 'first_name': 'Takanori', 'last_name': 'Shimizukawa', 'full_name': 'Takanori Shimizukawa'}
>>> serializer2 = UserSerializer(U, data={'first_name': 'Takayuki'}, partial=True)
>>> serializer2.is_valid()
True
>>> u2 = serializer2.save()
>>> serializer2.data
{'username': '2e6587340c5a41ada86b2e50d1ac2c08', 'email': 'test@example.com', 'first_name': 'Takayuki', 'last_name': 'Shimizukawa', 'full_name': 'Takayuki Shimizukawa'}
>>> serializer.data
{'username': '2e6587340c5a41ada86b2e50d1ac2c08', 'email': 'test@example.com', 'first_name': 'Takanori', 'last_name': 'Shimizukawa', 'full_name': 'Takanori Shimizukawa'}
>>> U is u2
True
ここで気になるのはなぜ
>>> U is u2
True
となるかだったが以下で答えがわかる。
>>> U.first_name
'Takayuki'
>>> serializer.data
{'username': '2e6587340c5a41ada86b2e50d1ac2c08', 'email': 'test@example.com', 'first_name': 'Takanori', 'last_name': 'Shimizukawa', 'full_name': 'Takanori Shimizukawa'}
>>>
つまり、serializer.save()で作ったオブジェクトとserializer.dataは別物なのだということなのだ。
serializer2 = UserSerializer(U, data={'first_name': 'Takayuki'}, partial=True)
及びserializer2.save()でUオブジェクト自体はUpdateされているが、serializer.dataはserializer.save()したわけではないので変更されていない。
普通同じオブジェクトに対して更新をかけるのに変数を変えてシリアライザを呼び出す……なんてことはしないと思うがこのあたりは間違えないようにしたい。
ModelSerializerは基本的には対象のModelに存在しているFieldに対してしか使えない。
例えば
class BookModel(models.Model):
title = models.CharField
price = models.intField
publish = models.DateTimeField
というModelがある場合、BookSerializerで扱えるのはtitle、price、publishのみである。
ただし、実際にはキャッシュを取得してそこから計算してあれこれして返されたものを引数として新たなFieldを作ってResponseとして返したい……なんてことが珍しくない。
例えばBookModelが図書館関係のAPIで使われていた場合、あるカテゴリに対してCacheからカテゴリ全体の貸し出し回数やリクエスト回数を取得しそれをもとに、その本がカテゴリの中でどれくらい貸し出し率が高いか……なんて情報を付け加えて出力したいといった感じ。
そういう時は以下のようにserializer.MethodField
を使ってやればいい。
class BookSerializer(serializer.ModelSerializer):
lending_rate = serializer.MethodField(read_only=True) # オプションも普通につけられる
ちなみにForeignObjectで定義されているFieldはリレーションしているModelのSerializerを直接指定する。
また、ForeignKeyで定義しているFieldでリレーション先のある値だけ使いたい場合も以下のようにして取得して使うことができる。
class BookSerializer(serializer.ModelSerializer):
lending_rate = serializer.MethodField(read_only=True)
# Model側でmodels.fieldで定義されていない場合はこちらで定義しないといけない。sourceでほしい値を指定
rent_user = serializer.ReadOnlyField(source='rent_by.name')
rental_result = RentalResultSerializer(read_only=True)
Field
Model定義するときに使うFieldと基本的には同じ。
ModelSerializerだとわかりやすいので以下参照。
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
# ここで扱うModelのFieldを定義する。そしてModelSerializerの場合はModelの各Field定義を見に行ってそれに応じたSerializerFieldを自動で使ってくれる。
fields = ['username', 'email', 'first_name', 'last_name', 'full_name'] # 以前はTupleだったがListで定義する
read_only_fields = ['username',] # 上に同じ、serializerMethodFieldなどで定義しているFieldはそちらの引数で。
extra_kwargs = {
'password': {'write_only': True},
}
特にModelSerializerの場合はwrite_only
オプションはextra_kwargs
以下で指定するように推奨されている。
また、fieldsで扱うFieldを定義しないとエラーなる(当然、serializerMethodField他で明示的に定義しているFieldも含める)
その他
応用編として実務に入って開発に関わるようになって以下のようなパターンも確認できた。
Serializerの入れ子
先程も出てきたがResponseに含めたいという理由でリレーションしているテーブルのフィールドを使いたい場合はそのフィールドのシリアライザをimportして定義し、class Metaのfieldに指定する.....という場合が往々にしてあるのでそういうSerializerの中で別のSerializerを呼ぶなんてケースもあるということを頭に入れておきたい。
モデル内に外部キーとして存在するフィールド、または自分の主キーが外部キーとして使われているフィールドでもOK。
class CustomBookSerializer(serializers.ModelSerializer):
# ex. 出版社を別テーブルで扱っていてリレーションしている場合
publisher = PublisherSerializer(read_only=True)
# ex. 著者を別テーブルで扱っていてリレーションしている場合
author = AuthorSerializer(many=True, read_only=True)
# 全く関係のない値をフィールドに指定したい場合(あとでget_segmentsのメソッドを作り、取得し、serializer.dataをreturnする)
segments = serializers.SerializerMethodField(read_only=True)
# BookModelが外部キーとして使われていて本の出版形態を管理する別テーブルがあり、そこの情報がほしい場合
book_functions = BookFunctionsSerializer(many=True, read_only=True)
validateのカスタマイズ
単純なCRUD処理であるならあまり必要がないが、上述のように外部のフィールドの値をResponseとして返すようにしている場合は、複雑なValidationをしたいということが往々にしてある。
そういう場合はカスタムを行う。
ポイントは以下
- validateメソッドを定義し、その中に別で定義した各フィールドのvalidateメソッドを呼び出すようにする。
- 変数や各フィールドのValidationメソッドはprivateにする
- 各フィールドのValidationメソッドは@staticmethodのデコレータをつける
- 基本的にValidationが通った場合は空のdictを返すようにする
雑に例を書く、大まかなので構造を把握する感じで。
def validate(self, attrs):
attrs = super().validate(attrs)
request = self.context['request']
# 空のdictを用意
_errors = {}
# カスタムした各Validationメソッドの呼び出し、通れば空のdictのままでErrorになったらそのValidationエラーが返される仕組み
_errors = self._validate_publisher(_errors, request.data)
_errors = self._validate_book_sale_store_ids(_errors, request.data)
if any(_errors):
raise serializer.ValidationError(_errors)
return attrs
@staticmethod
def _validate_publisher(_errors, request.data):
_publisher = request_data.get('publisher')
# 具体的にValidationを書く、今回はRequestから空で送られてきて無いかチェック
if not _publisher:
_errors['publisher'] = '著者の情報は必須です!'
return _errors
# textareaに店舗IDを入力し、その本が売れている店舗の情報を返したいなんてときに
@staticmethod
def _validate_book_sale_store_ids(_errors, request_data):
# textareaから送られてきたrequest.dataは文字列になる
_sale_store_param: str = request_data.get('sale_store_ids')
if not _sale_store_param:
return _errors
# textareaは改行区切りやカンマ区切りでデータが来る、改行区切りの場合はこれでリストに
_split_sale_store_ids = _sale_store_param.splitlines()
if not all(c.isdecimal() for s_id in _split_sale_store_ids):
_errors['Validationエラー'] = '入力は改行区切りで送信してください'
return _errors
# 数値のリストに変換。リスト内包表記で文字列のリストの各要素をintにして格納
_sale_store_ids: List[int] = [int(sale_store_id) for cm_id in _sale_store_param.splitlines()]
# ModelにIN句で問い合わせ。DRFでIN句を表現するには以下のように書く
# values_listであるカラムの値だけをListで取得できる。ただし、TypeはQuerySetなのでlistに変換する
_sale_store_ids_db: List[int] = list(BookStore.objects.filter(id__in=_sale_store_ids).values_list('id', flat=True))
id_diff_list: List[int] = list(set(_sale_store_ids) - set(_sale_store_ids_db))
# id_diff_listが存在するということは入力値にDBにには存在しない値が含まれていたことになるのでValidationする
if id_diff_list:
_errors['Validationエラー'] = f'こちらのIDは存在しません。 {id_diff_list}'
return _errors
ModelViewSetを使う場合
単純なCRUD処理ではModelSerializerではなくMixinやModelViewSetを継承してModelViewSetをカスタムして使うことも少なくない。
その場合は例えばCreateの処理をカスタマイズしたいとなったらSerializerにdef create(self, validated_data):
と定義してそこに処理を書けばできる。
例えばModelViewSetなModelの新規登録に合わせてそれとリレーション関係にある中間テーブルにも任意のデータをゴニョゴニョして登録する必要がある.....という場合に使うことがあるので覚えておく。
当然、Updateとか他の処理でも同じことがいえる。
Serialzierの分割
あまりにSerializerがFatになるようであり、かつ機能やmodule全体で共通化できるような処理であればlogicとして分離し、それをSerialzier内で呼び出す形で実装するというパターンもあった。
おわりに
実務で始めてSerializerに本格的に触れて、単に入出力のValidationを行っているのではなく、これとセットでViewは成り立っているところが大きいんだなと感じた。
APIの仕様を把握するには避けては通れないところなので引き続き精進したい。
参考
[Django REST Framework] Serializer の 使い方 をまとめてみた
現場で使える Django REST Framework の教科書(商品リンク)