DjangoRESTFramework (以降 DRF という) を最近良く使っているのですが 設定項目が多すぎて情報探すのに時間がかかっちゃうので、自分なりにまとめてみました。
2月の後半くらいに書いてたんですが、ブログの改修に時間がかかりすぎて公開が遅れたのは内緒。
個人的な感覚ですが、このライブラリの機能を大きく分けると
Serializer
, View
に分かれます。
本当は全部通しで書きたかったんですが、長くなりすぎたので
View
の部分は別の記事に分割します。
- info
- ビュー については View の使い方をまとめてみた を参照してください。
- 初めて触る方は 先にビュー編を見ることをオススメします。 シリアライザ単体で使うことはまずないと思うので。
- この記事の先頭に書いてあったインストールの手順とかはそちらの記事に移動しました
- 基本的に情報は v3.7.7 時点の 公式ドキュメント を参考にしました。 一部以下のブログも参照させてもらいました。
- Django REST framework 超入門
- Django REST Frameworkで再帰的なリレーションを持つモデルを返すAPIを作成したい
自分で言うのも何ですが分割しても尚長いです。クオリティに自信はありませんが、長さには自信があります。
7時半から空手の稽古があるからあとで読む? 今日は休め
シリアライザとは
Serializer は データ の入出力を扱い、モデルへの橋渡しをするクラスです。
(プレーンな)Djangoには Form
クラスがありましたが、あれの
API用 がシリアライザと考えればよいでしょう。
というのも、実は API を扱う場合 Django Form はあまり適していません。
a=1&b=2
のような x-www-form-urlencoded
形式での入力を前提としているFormでは JSON や XMLのような複雑なデータは処理できませんし、 (大抵)レスポンスも JSON
などで返却するため、Form オブジェクト のHTML レンダリング
も使いませんよね。 このようにFormではAPI実装において過不足が多いのです。
Serializer は
- 複雑な入力値をモデルに合わせてバリデーションしてレコードに伝えたり(入力)
- Model(レコード)を適切な形式にフォーマットしたり(出力)
と言った具合に、 APIの リクエスト
/ レスポンス
に特化した機能を提供します。
- info
- Serializer では
入力専用
,出力専用
, またはいずれ
にも機能するものがあります。 - この記事では
いずれにも機能するもの
を除き、できるだけ明記していきます。 入力=リクエスト
,出力=レスポンス
の方向ですのでご了承ください。
- Serializer では
シリアライザの種類
Serializer にはいくつか種類があります。
serializers.Serializer
もっとも単純なシリアライザです。
チュートリアルに従って以下のようなクラスがある前提で話を進めていきます。
from datetime import datetime class Comment(object): def __init__(self, email, content, created=None): self.email = email self.content = content self.created = created or datetime.now()
これに対応したシリアライザを以下のように定義できます。
from rest_framework import serializers class CommentSerializer(serializers.Serializer): email = serializers.EmailField() content = serializers.CharField(max_length=200) created = serializers.DateTimeField() def create(self, validated_data): return Comment(**validated_data) def update(self, instance, validated_data): instance.email = validated_data.get('email', instance.email) instance.content = validated_data.get('content', instance.content) instance.created = validated_data.get('created', instance.created) return instance
書いてあることは単純で、フィールドと保存時の動作がメソッドとして定義されているだけです。 フィールドには値に対応した型を記述します。これはFormと同じですね。
実際にデシリアライズしてみます。 デシリアライズとは入力データを元になんらかのオブジェクトを作成する(あるいはそのためにデータを適した形にする)ことです。
- warning
- デシリアライズでは入力値は辞書に準ずる型でなくてはいけません。 たとえば QueryDictなど
まず、 data 引数に入力データを与えて、 is_valid()
メソッドをコールします。
>>> serializer = CommentSerializer(data={'email': 'crohaco@example.com', 'content': 'なにか', 'created': '2016-01-27T15:17:10.375877Z'}) >>> serializer.is_valid() True >>> serializer.validated_data OrderedDict([('email', 'crohaco@example.com'), ('content', 'なにか'), ('created', datetime.datetime(2016, 1, 27, 15, 17, 10, 375877, tzinfo=<UTC>))])
続いて、この入力データをオブジェクトとして出力してみます。
それを行うのが シリアライザインスタンスの save()
メソッドです。
(saveとは言っていますが、単なるSerializerでは永続化はされません)
このメソッドにより先程シリアライザに定義した create
と
update
メソッドが 状況に応じて 透過的に呼び出されます。
それぞれ新規作成・更新するためのメソッドで、お察しの通り入力専用の機能です。
>>> comment = serializer.save() >>> comment <Comment object at 0x10f6fbb00> >>> comment.email 'crohaco@example.com'
そして、この「状況」というのが 第一引数の有無です
# .save() will create a new instance. serializer = CommentSerializer(data=data) # .save() will update the existing `comment` instance. serializer = CommentSerializer(comment, data=data)
- 第一引数がない場合
create
メソッドがコールされる (新規作成)
- 第一引数がある場合
update
メソッドがコールされる (更新)- 第一引数は update メソッドの 第一引数に引き継がれる
partial
引数がTrueの場合、第二引数(data)ですべての値を指定しなくても、第一引数のオブジェクトから補完される(つまり一部の値だけでいいということ)
これでデシリアライズは完了です。
受け取った値を参照する場合は validated_data
という属性を使います。
これにはバリデーション後の値が格納されていますが、バリデーションが失敗すると空になります。(バリデーションについては後述します)
そしてバリデーションを行うための メソッドが is_valid
です。
よって、このメソッドを実行した後でなければ validated_data
属性を参照することはできませんし、
validated_data
を元にオブジェクトを生成する save
メソッドも同様に呼び出すことはできません。
バリデーションを行わずにsaveを呼び出すと
AssertionError: You must call `.is_valid()` before calling `.save()`.
のように怒られます。
- warning
data
(出力専用)属性を参照した後にはsave()
はコールできません。おそらく
data
は既存の情報読み出し(出力)に使われる属性なので、保存(入力)と同時に使われることはありえないという思想なんだと思います。どうしても参照する必要があるなら、
validated_data
にしましょう。>>> serializer.data {'email': 'leila@example.com', 'content': 'foo ban', 'created': '2016-01-27T15:17:10.375877Z'} >>> serializer.save() Traceback (most recent call last): File "<console>", line 1, in <module> File "/root/venv/lib/python3.6/site-packages/rest_framework/serializers.py", line 198, in save "You cannot call `.save()` after accessing `serializer.data`." AssertionError: You cannot call `.save()` after accessing `serializer.data`.If you need to access data before committing to the database then inspect 'serializer.validated_data' instead.
- info
推奨されている方法ではありませんが、バリデーションに失敗しても値だけを参照したい場合は以下のようにするとバリデーションに失敗していないデータが取れます。
{key: value for key, value in serializer.data.items() if key not in serializer.errors})
ただこの方法は失敗していないデータを取得するだけで、バリデーションメソッドによって変換された値が取れるわけではない(はずな)のであくまで自己責任でお願いします。
- info
save()
メソッドはバリデーションをバイパスして serializer に直接を値を渡せます。具体的に言うと
save()
に渡した引数はvalidated_data
を上書きし、create
やupdate
メソッドに渡っていきます。 (該当コード)>>> serializer = CommentSerializer(data={'email': 'leila@example.com', 'content': 'foo bar', 'created': '2016-01-27T15:17:10.375877'}) >>> serializer.is_valid() True # キーワード引数として指定する >>> obj = serializer.save(email='aaaaaaaaaaa') >>> obj.email 'aaaaaaaaaaa'
上記では Eメール を正しくない値に書き換えていますが、本来は
save()
にはチュートリアルのようにログインユーザのような信頼できる値を渡すべきです。serializer.save(owner=request.user)
これまで説明してきたデシリアライズに対し、
オブジェクトを元にデータを読み出す処理を(あるいはそれを整形する処理も含めて) シリアライズ
といいます。
serializers.Serializer
はシリアライズ対象がオブジェクトであれば何でも構いません。
(後述する ModelSerializer
はDjangoモデル でなくてはいけない)
comment オブジェクトをシリアライズしてみます
>>> comment = Comment(email='leila@example.com', content='fooooo') # 第一引数に与える >>> serializer = CommentSerializer(comment) # 読み出された値は data 属性に入ってる >>> serializer.data {'email': 'leila@example.com', 'content': 'fooooo', 'created': '2018-09-18T12:12:20.744635Z'}
と、以上が Serializer
クラスとシリアライズ(デシリアライズ)の簡単な説明でした。
- info
- お詫び: シリアライズとデシリアライズの説明が逆になっていました。
- 気づいてくれた aki_yok さんに感謝
serializers.ModelSerializer
ようやく本命です。
おそらく、これが一番利用されることになる Serializer です。
これは Model に紐づく Field を Serializer のフィールドとして自動的に定義してくれる君です。 Formでいうなら ModelForm みたいなもんでしょうか。
今回は User モデル用のシリアライザを用意します。
from django.contrib.auth.models import User from django.contrib.auth.hashers import make_password from rest_framework import serializers class UserSerializer(serializers.ModelSerializer): # SerializerMethodField は get_xxxx ってなっているメソッドをコールする full_name = serializers.SerializerMethodField() class Meta: model = User fields = ('username', 'email', 'first_name', 'last_name', 'full_name') read_only_fields = ('username',) extra_kwargs = { 'password': {'write_only': True}, } def get_full_name(self, instance): return instance.get_full_name() # User に元からあるメソッドを呼び出してるだけ def create(self, validated_data): password = validated_data.pop('password', 'something') # そのまま使っちゃだめだよ user = User(username=uuid.uuid4().hex, password=make_password(password), **validated_data) user.save() return user
SerializerMethodField がありますが、これは実際に動的に作成したい値を定義するフィールドです。
get_{フィールド名}
として定義されているメソッドをコールします。
これらは共に出力専用属性です。入力値の整形には、後述する validate\*
, validated_data
属性を使いましょう。
単純なSerializerでも使えますが、 ModelSerializerで使う場合、 fields に含まれてないと以下のようなエラーになります。
なお、自動的に read_only なフィールドとなるため fields
だけに指定すれば良いです。
AssertionError: The field 'full_name' was declared on serializer UserSerializer, but has not been included in the 'fields' option.
少し脱線したので話を戻します。
Serializer クラスとの違いは Meta
です。Metaクラスに必須なのは model
です。
(ここでいう Metaクラスは Python の メタクラス
とは違うので注意)
- warning
- フィールドの制限設定には
write_only_fields
がありますが、 これらの利用は 非推奨になり、これからはextra_kwargs
を使うらしいです。 extra_kwargs
は 辞書型 で フィールドをキーに取ります。'write_only': True
を指定します- Serializer から値を入れたいけど、読み出しはしたくない場合に指定します。
- すぐに思い浮かぶのはパスワードとかEmailアドレスなどの個人情報関連のデータですね。
(訂正)
read_only
はread_only_fields
を引き続き使うようです。
- フィールドの制限設定には
実際に入力値を使ってレコードを作ってみましょう。
>>> 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': '4d46f60c84be4ae09090bb1751b9e289', 'email': 'test@example.com', 'first_name': 'Takanori', 'last_name': 'Shimizukawa', 'full_name': 'Takanori Shimizukawa'} # 間違っていたので partial で名前だけを指定して修正 >>> serializer2 = UserSerializer(u, data={'first_name': 'Takayuki'}, partial=True) >>> serializer2.is_valid() True >>> u2 = serializer2.save() >>> serializer2.data {'username': '4d46f60c84be4ae09090bb1751b9e289', 'email': 'test@example.com', 'first_name': 'Takayuki', 'last_name': 'Shimizukawa', 'full_name': 'Takayuki Shimizukawa'} >>> u is u2 # 実体は同じ(もちろんIDも) True # パスワードは設定されているが write_only なので 読み出されていない >>> u.check_password('something') True >>> u.password 'pbkdf2_sha256$100000$Va5TbTa8wpGP$zkyS6nDED3K19+KDjJkOfR3oC9f1w5bxxggsHBjg1pw='
ちなみに、シリアライザ全般に言えることですが、レコードのフィルタ(検索)条件は後述する View に定義するもので、シリアライザには書きません。 よってqueryset をここにずらずらと書くことはほとんどないと思います。
また、 データが複数になる(list形式)場合は many=True
引数を指定するとまとめて処理できます。
many=True
の有無により期待される型が異なるので十分に注意しましょう。
>>> serializer.errors {'non_field_errors': ['Invalid data. Expected a dictionary, but got list.']}
ただし、保存処理は create や update を複数回実行しているにすぎないため、効率が良いとはいえません。 こういった場合は以下の ListSerializer を使って定義した方が良いかもしれません。
- info
- ModelSerializer はモデルに存在する属性しか対象にできません。
- もしモデルにないフィールドをシリアライザに生やすと
django.core.exceptions.ImproperlyConfigured: Field name message is not valid for model TargetModel.
のようなエラーになります。 - これを回避するには
SerializerMethodField
を使うか、Model に@property
デコレータでフィールドを追加する方法があります。 - Django REST Framework: adding additional field to ModelSerializerI want to serialize a model, but want to include an additional field that requires doing some database lookups on the model instance to be serialized: class FooSerializer(serializers.ModelSerialize...https://stackoverflow.com/questions/18396547/django-rest-framework-adding-additional-field-to-modelserializer
serializers.ListSerializer
ListSerializer は 複数のモデルを扱うことを前提にしたシリアライザです。
前述した many=True
を引数に指定した Serializer インスタンスと似たような動きをします。
複数受け取ることが前提なので、インスタンス化の際に many=True
を指定する必要がありません。
- warning
many=True
のときとは違い、create
,update
メソッドで受け取るvalidated_data
も複数形(list型)になります。
create
や update
メソッドを省略した場合は child のシリアライザが要素の回数だけ呼び出されます。
今回は create メソッドで受け取った validated_data
(list型)から モデルインスタンスを複数作りバルクインサートしてみます。
この場合 child
の create メソッドは呼び出されません。
# さっきの続きから class UsersSerializer(serializers.ListSerializer): child = UserSerializer() def create(self, validated_data): users = [] for user_data in validated_data: password = user_data.pop('password', 'something') # そのまま使っちゃだめだよ users.append(User(username=uuid.uuid4().hex, password=make_password(password), **user_data)) return User.objects.bulk_create(users)
ListSerializer では child を指定する必要なことに注意してください。 ないと AssertionError の刑です。
assert self.child is not None, '`child` is a required argument.' AssertionError: `child` is a required argument.
child はクラス変数として指定することもできますし、引数として入力することもできます。 どちらがいいかは場合によりますが、 Serializer が可変でなければクラス変数に指定するのがよいでしょう。
いずれにおいても、インスタンスを指定するということに注意しましょう。 インスタンス化しない場合もまた AssertionError なのです。
assert not inspect.isclass(self.child), '`child` has not been instantiated.' AssertionError: `child` has not been instantiated.
ListSerializer や、 many=True
のシリアライザの場合、入力値は リスト形式で指定します。
>>> serializer = UsersSerializer(data=[ ... {'first_name': 'Takayuki', 'last_name': 'Shimizukawa', 'username': 'test', 'email': 'test@example.com'}, ... {'first_name': 'Takanori', 'last_name': 'Suzuki', 'username': 'test', 'email': 'test2@example.com'}, ... ] # ここで child 引数に指定することもできる。引数のほうが優先。 ... #, child=UserSerializer() ... ) >>> serializer.is_valid() True # 保存までできた >>> sh, ta = serializer.save() >>> sh.get_full_name() 'Takayuki Shimizukawa' >>> ta.get_full_name() 'Takanori Suzuki' # 発行されたクエリを見るために connection.queries を参照する >>> from django.db import connection >>> print(connection.queries[-1]['sql']) # 一応バルクインサートになってますね INSERT INTO "auth_user" ("password", "last_login", "is_superuser", "username", "first_name", "last_name", "email", "is_staff", "is_active", "date_joined") SELECT 'pbkdf2_sha256$100000$7hAtQ2FFWVJ5$lC/SefOqFH1QzZ4ydzOz3s5ee6pMK7EQSrH0g1zyPVk=', NULL, 0, '8820a1d85233443e91c23847a9bf8928', 'Takayuki', 'Shimizukawa', 'test@example.com', 0, 1, '2018-01-23 10:38:38.718964' UNION ALL SELECT 'pbkdf2_sha256$100000$mv7KOwcvBHyM$EjEooDwSfwiaOpEX/EQ4L33vbUjFUImTm27whpg29dQ=', NULL, 0, '825392f05208496ea05971b5869ee049', 'Takanori', 'Suzuki', 'test2@example.com', 0, 1, '2018-01-23 10:38:38.824717'
- ほかにも serializers.HyperlinkedModelSerializer というのがあるそうですが、面倒なのでやめときます。
- 公式リファレンスはこちら: serializers
フィールド
シリアライザに指定できるフィールドについて解説します。 CharField や DateField などの基本的なフィールドは Form とかモデルと同じなので除外です。
Serializer fields を見たほうが早いかもしれませんが、重要なものは解説しておきましょう。
共通する引数
詳細は Core arguments のリンクを参照してほしいのですが、 Serializer の Field には共通して指定できる引数というものが幾つかあります。
ここでは重要なものに絞って説明します。
- read_only
- 出力専用のフィールドか否かを Boolean で指定します。
(デフォルトは
False
)
- 出力専用のフィールドか否かを Boolean で指定します。
(デフォルトは
- write_only
- 入力専用のフィールドか否かを Boolean で指定します。
(デフォルトは
False
)extra_kwargs
でも指定できます。
- 入力専用のフィールドか否かを Boolean で指定します。
(デフォルトは
- required
- 値の指定が必須かどうかを Boolean で指定します。
- デフォルトは
True
ですが、False
を指定すると入力値に該当フィールドがなくてもスルーしてくれます。 - これは入力専用の設定です。
- デフォルトは
- 値の指定が必須かどうかを Boolean で指定します。
- allow_null
None
の入力を許容する場合はTrue
を指定します。 (デフォルトはFalse
です)- このオプションを有効にすると 次に説明する
default
オプションはNone
になりますが、デシリアライズのデフォルト値がnull
になるわけではありません。 - 何の役に立つかといえば、実は
patch
リクエストを多用しているようなアプリケーションでは重宝します。partial=True
を指定したシリアライザでレコードをnull
に更新する場合、値がないと判断されるとレコードの値で補完されてしまいます。- こういったケースでは
allow_null
オプションを有効にすることでフィールドをnull
にできます。 - Django Rest Framework: How to set a field to null via PATCH request? - Stack Overflow
- このオプションを有効にすると 次に説明する
- warning
当然ですが、これが指定されたフィールドは
partial=True
の部分更新はできなくなることを頭に入れておきましょう。別の箇所でフィールドの部分更新で無視する必要がでてきた場合、以下のいずれかのような対処が考えられます
- 別のシリアライザを定義する
allow_null
を無効にし、更新リクエストはPUT
に変えて全体を更新する
- default
- 値が省略された場合に補完する値を指定します。
- この引数が指定された場合
required
は 自動的にFalse
になります。- これらを共に有効にするとエラーが発生します。
- これは partial_update のときには機能しません。
- ちなみに default によって設定された値はバリデーションの対象にはならないので注意が必要です。
- この引数が指定された場合
- 値が省略された場合に補完する値を指定します。
- source
- 通常、シリアライザの属性名は対象のオブジェクトの属性名と一致しますが、合わせられないこともあります。そんなときはこの引数で別の属性名を指定してあげましょう。
- validators
バリデータをリスト形式で指定します。詳しくはバリデーションセクション を参照ください。
バリデータは後述するのでそれ以外について動作を確認してみましょう。
class MyCallable(object): def __call__(self): print('ここで', self.field, 'を使ってごにょごにょする') return 'something' def set_context(self, field): self.field = field class SomethingSerializer(serializers.Serializer): a = serializers.EmailField(default=MyCallable()) b = serializers.IntegerField(read_only=True) c = serializers.CharField(write_only=True, required=False) d = serializers.RegexField(regex='[1-9].*', required=True) e = serializers.DateField(source='e2')
>>> ss1 = SomethingSerializer(data={'b': 2, 'c': 3, 'd': '1', 'e': '2000-01-01'}) >>> ss1.is_valid() ここで EmailField(default=<MyCallable object>) を使ってごにょごにょする True # a には デフォルト値が入る # b は 出力専用なので無視される # 文字列フィールド c に 数値を指定すると文字列型に矯正される # e として受け取った値は e2 に渡っている >>> ss1.validated_data OrderedDict([('a', 'something'), ('c', '3'), ('d', '1'), ('e2', datetime.date(2000, 1, 1))]) # required=False の c は 省略してもOK >>> ss2 = SomethingSerializer(data={'a': 'a@example.com', 'd': '4', 'e': '2000-01-01'}) >>> ss2.is_valid() True # required=False の d は 省略不可 >>> ss3 = SomethingSerializer(data={'a': 'a@example.com', 'c': '3', 'e': '2000-01-01'}) >>> ss3.is_valid() False >>> ss3.errors {'d': ['This field is required.']}
続いて値の読み出し
# めんどくさいので Mock を使う >>> from unittest.mock import Mock >>> something = Mock(a='a', b='10', c='c', d='d', e='e', e2='e2') >>> ss4 = SomethingSerializer(something) # 数値フィールドである b は 数値に 矯正される # c は 入力専用なので無視される # e は e2 フィールドの値が読み出されている >>> ss4.data {'a': 'a', 'b': 10, 'd': 'd', 'e': 'e2'}
雑多なフィールド
これらのフィールド同士に特別な結びつきはありません。
以下のフィールドについて解説します。
- HiddenField
- 入力時も出力時も値を受け取らないフィールドです。
- default 引数 が必須です。
- 入力専用のフィールドです。
- 入力時も出力時も値を受け取らないフィールドです。
- SerializerMethodField
- フィールドの値を加工して別の値を作ることができるフィールドです。
- 出力専用のフィールドです。
get_
プリフィックスがつくメソッドを呼び出して、返却値を出力します。- 引数にはインスタンスが入ります。
- フィールドの値を加工して別の値を作ることができるフィールドです。
使ってみます。気分によって 顔の色が変わるシリアライザーを考えてみます。
class ComplexionSerializer(serializers.Serializer): face = serializers.SerializerMethodField() feeling = serializers.HiddenField(default=0) def get_face(self, instance): # それぞれ 黃, 赤, 緑, 青 return ['k', 'r', 'g', 'b'][instance.feeling]
>>> ts1 = ComplexionSerializer(data={'face': 'k', 'feeling': 2}) >>> ts1.is_valid() True >>> ts1.validated_data OrderedDict([('feeling', 0)]) # 外から気分を左右されない
data に指定した値はいずれも無視されていますね。 HiddenField は 入力専用だけど入力値は無視する、そんな不思議なフィールドです。
default には 呼び出し可能なオブジェクトを指定できるので 本来は環境要因 によって変動するような値を作る関数などを指定するのでしょう。
続いて出力してみます。
class Tell(object): def __init__(self, feeling=None): self.feeling = feeling
>>> tell_k = Tell(feeling=0) >>> tell_r = Tell(feeling=1) >>> tell_g = Tell(feeling=2) >>> tell_b = Tell(feeling=3) >>> ComplexionSerializer(tell_k).data {'face': 'k'} # 黄 >>> ComplexionSerializer(tell_r).data {'face': 'r'} # 赤 >>> ComplexionSerializer(tell_g).data {'face': 'g'} # 緑 >>> ComplexionSerializer(tell_b).data {'face': 'b'} # 青
今度は feeling フィールドは無視されて 顔色だけが出力されていますね。
外部に紐づくフィールド
前述した ModelSerializer を使ってる場合、 ForeignKey (外部キー)で 別テーブルを参照していることはよくあります。 その時、紐付いたレコードをどのように表示するかを指定するのがこのフィールドです。今回は 以下の 2つを使ってみます。
この記事ではDjango がデフォルトで用意してくれてるモデルだけを使って説明したいので今回は
Permission
モデルを例にとって説明してみます。 関連コードは
auth/models.py,
content_types/models.py
を参照ください。
何に使うモデルかというと ユーザごとのアクセス権を設定します。(あんまり使わないやつ)
APIの対象としてはあまりよい例ではないかもしれませんがご勘弁ください。
from rest_framework import serializers class PermissionSerializer(serializers.ModelSerializer): """デフォルト""" class Meta: fields = ('name', 'content_type', 'codename') model = Permission class PermissionSerializer2(serializers.ModelSerializer): """StringRelatedFieldを使ってみる""" content_type = serializers.StringRelatedField() class Meta: fields = ('name', 'content_type', 'codename') model = Permission class PermissionSerializer3(serializers.ModelSerializer): """PrimaryKeyRelatedFieldを使ってみる""" content_type = serializers.PrimaryKeyRelatedField(queryset=ContentType.objects.filter()) class Meta: fields = ('name', 'content_type', 'codename') model = Permission
まずは出力をしてみましょう。 Permission と それに紐づく ContentType
レコードを 生成
します。
# 以下2行は以降省略 from django.contrib.auth.models import User, Permission from django.contrib.contenttypes.models import ContentType ct = ContentType.objects.create(app_label='test', model='auth.User') p = Permission.objects.create(name='test permission', content_type=ct, codename='test_perm') >>> ps = PermissionSerializer(p) >>> ps2 = PermissionSerializer2(p) >>> ps3 = PermissionSerializer3(p) # プライマリキーが出力される >>> ps.data {'name': 'test permission', 'content_type': 12, 'codename': 'test_perm'} # 紐づくコンテンツの __str__ or __unicode__ が出力される >>> ps2.data # content_type には str(ct) が入る {'name': 'test permission', 'content_type': 'auth.User', 'codename': 'test_perm'} # プライマリキー が出力される >>> ps3.data {'name': 'test permission', 'content_type': 12, 'codename': 'test_perm'}
続いて入力の確認です。
>>> ps = PermissionSerializer(data={'name': 'input test1', 'content_type': 12, 'codename': 'input test1'}) >>> ps2 = PermissionSerializer2(data={'name': 'input test2', 'content_type': 12, 'codename': 'input test2'}) >>> ps3 = PermissionSerializer3(data={'name': 'input test3', 'content_type': 12, 'codename': 'input test3'}) >>> ps.is_valid() True >>> ps2.is_valid() True >>> ps3.is_valid() True >>> ps.save() <Permission: test | auth.User | input test1> >>> ps2.save() Traceback (most recent call last): File "/root/venv/lib/python3.6/site-packages/django/db/backends/utils.py", line 85, in _execute return self.cursor.execute(sql, params) File "/root/venv/lib/python3.6/site-packages/django/db/backends/sqlite3/base.py", line 303, in execute return Database.Cursor.execute(self, query, params) sqlite3.IntegrityError: NOT NULL constraint failed: auth_permission.content_type_id >>> ps3.save() <Permission: test | auth.User | input test3>
StringRelatedField を使ってるシリアライザはエラーになりました。 実は StringRelatedField は 読み込み専用なんです。この記事の用語で言うと出力専用です。
また、 PrimaryKeyRelatedField は インスタンス
を期待しないので注意してください。
>>> ps3re = PermissionSerializer3(data={'name': 'input test3', 'content_type': ct, 'codename': 'input test3'}) >>> ps3re.is_valid() False >>> ps3re.errors {'content_type': ['Incorrect type. Expected pk value, received ContentType.']}
デフォルトの状態だと 未指定 と PrimaryKeyRelatedField
は同じ挙動ですが、与える引数によって異なる振る舞いを与えられます。
今回は many=True
を M2M で被参照関係にある
user_set
フィールドを指定してみましょう。
class PermissionSerializer4(serializers.ModelSerializer): """PrimaryKeyRelatedFieldを使ってみる""" user_set = serializers.PrimaryKeyRelatedField(queryset=User.objects.filter(is_active=True), many=True) class Meta: fields = ('name', 'content_type', 'codename', 'user_set') read_only_field = ('content_type',) model = Permission
# ユーザはこの3レコード >>> User.objects.all().values('id', 'username', 'is_active') <QuerySet [{'id': 25, 'username': 'haru', 'is_active': False}, {'id': 26, 'username': 'shimizukawa', 'is_active': True}, {'id': 27, 'username': 'takanory', 'is_active': True}]> >>> ps4 = PermissionSerializer4(p) >>> ps4.data {'name': 'test permission', 'content_type': 12, 'codename': 'test_perm', 'user_set': [25, 26, 27]} >>> ps4 = PermissionSerializer4(data={'name': 'input test3', 'content_type': 12, 'codename': 'input test3', 'user_set': [25, 26, 27]}) >>> ps4.is_valid() False >>> ps4.errors {'user_set': ['Invalid pk "25" - object does not exist.']} >>> ps4 = PermissionSerializer4(data={'name': 'input test4', 'content_type': 12, 'codename': 'input test4', 'user_set': [26, 27]}) >>> ps4.is_valid() True >>> p4 = ps4.save() >>> p4.user_set.all() <QuerySet [<User: shimizukawa>, <User: takanory>]>
おー、これで ManyToManyField の保存もできるわけですね。`many=True`
を忘れると Incorrect type
って言われるので注意。
queryset
引数は入力(バリデーション)にしか機能しないようなのでご注意ください。
また、 Serializer
を指定するとその Serializer で自動的に
シリアライズ されたりします。
RelatedField の代わりに シリアライザを指定してみましょう。
class ContentTypeSerializer(serializers.ModelSerializer): class Meta: model = ContentType fields = ('app_label', 'model',) class PermissionSerializer5(serializers.ModelSerializer): content_type = ContentTypeSerializer() class Meta: fields = ('name', 'content_type', 'codename') model = Permission
>>> ps5 = PermissionSerializer5(p) >>> ps5.data {'name': 'test permission', 'content_type': OrderedDict([('app_label', 'test'), ('model', 'auth.User')]), 'codename': 'test_perm', } >>> ps5 = PermissionSerializer5(data={ ... 'name': 'input test1', ... 'content_type': 12, ... 'codename': 'input test1' ... }) >>> ps5.is_valid() False >>> ps5.errors {'content_type': {'non_field_errors': ['Invalid data. Expected a dictionary, but got int.']}}
serializer を指定したフィールドは入力に辞書を期待します。モデルインスタンスでもだめです。
>>> ps5 = PermissionSerializer5(data={ ... 'name': 'input test1', ... 'content_type': {'app_label': 'test2', 'model': 'auth.User'}, ... 'codename': 'input test1' ... }) >>> ps5.is_valid() True >>> p5 = ps5.save() Traceback (most recent call last): File "<console>", line 1, in <module> File "/root/venv/lib/python3.6/site-packages/rest_framework/serializers.py", line 214, in save self.instance = self.create(validated_data) File "/root/venv/lib/python3.6/site-packages/rest_framework/serializers.py", line 903, in create raise_errors_on_nested_writes('create', self, validated_data) File "/root/venv/lib/python3.6/site-packages/rest_framework/serializers.py", line 797, in raise_errors_on_nested_writes class_name=serializer.__class__.__name__ AssertionError: The `.create()` method does not support writable nested fields by default. Write an explicit `.create()` method for serializer `builtins.PermissionSerializer5`, or set `read_only=True` on nested serializer fields.
デフォルトだと入れ子になった要素は保存できないと文句を言われました。 入れ子の要素を再帰的に保存するためにはそのように create メソッドを定義してパラメータをゴニョゴニョする必要があります。 このような使い方は想定されていないので、きれいには書けないのですね。
read_only=True
を指定しろとエラーメッセージさんがおっしゃってるのでこれも試してみます。
class PermissionSerializer6(serializers.ModelSerializer): content_type = ContentTypeSerializer(read_only=True) class Meta: fields = ('name', 'content_type', 'codename') model = Permission
>>> ps6 = PermissionSerializer6(p) >>> >>> ps6.data {'name': 'test permission', 'content_type': OrderedDict([('app_label', 'test'), ('model', 'auth.User')]), 'codename': 'test_perm'} >>> ps6 = PermissionSerializer6(data={ ... 'name': 'input test1', ... 'content_type': 12, ... 'codename': 'input test1' ... }) >>> ps6.is_valid() True >>> p6 = ps6.save() Traceback (most recent call last): File "/root/venv/lib/python3.6/site-packages/django/db/backends/utils.py", line 85, in _execute return self.cursor.execute(sql, params) File "/root/venv/lib/python3.6/site-packages/django/db/backends/sqlite3/base.py", line 303, in execute return Database.Cursor.execute(self, query, params) sqlite3.IntegrityError: NOT NULL constraint failed: auth_permission.content_type_id
12 を ID として認識してくれるなんて都合の良い動作はしてくれないようです。辛い。
read_only が使えるのはあくまで NULL=True
のフィールドに限るということでしょう。
read_only
を指定すると 入力値はすべて無視されるので注意してくださいね。
出力時は Serializer で、入力は ID で受けてほしいんですがどうしたらいいんでしょうか。
別のフィールドを定義して、source 属性で振り分けるということですね。 あー、それ俺も思い浮かんだわー、2秒位で浮かんだわー。2時間しか寝てないわー。
class PermissionSerializer7(serializers.ModelSerializer): content_type = ContentTypeSerializer(read_only=True) content_type_id = serializers.PrimaryKeyRelatedField( queryset=ContentType.objects.filter(), source='content_type', write_only=True) class Meta: fields = ('name', 'content_type', 'codename', 'content_type_id',) model = Permission
>>> ps7 = PermissionSerializer7(p) >>> ps7.data {'name': 'test permission', 'content_type': OrderedDict([('app_label', 'test'), ('model', 'auth.User')]), 'codename': 'test_perm'} >>> ps7 = PermissionSerializer7(data={ ... 'name': 'input test7', ... 'content_type_id': 12, ... 'codename': 'input test7' ... }) >>> ps7.is_valid() True >>> ps7.validated_data OrderedDict([('name', 'input test7'), ('codename', 'input test7'), ('content_type', <ContentType: auth.User>)]) >>> p7 = ps7.save() >>> p7 <Permission: test | auth.User | input test7>
できましたね。 ManyToManyField
も同様にできるはず。ひとまず RelatedField 関係はこれで決着。
複合フィールド
複数のフィールドをまとめたフィールドのことです。
ListField
, DictField
, JSONField
があります。
ListField と DictField はいずれも、 child 属性に対し Field, もしくは Serializer の指定が必要です。
ListSerializer
と似ていますね。つまり、子要素の型が揃っている場合にのみ利用できます。
ここでは継承してクラスとして定義する方法を中心に説明しますが、そのままインスタンスにもできます。(定義例は後述)
まずは ListField から。
from rest_framework import serializers class ScoreListField(serializers.ListField): child = serializers.IntegerField(max_value=100)
フィールド単体で検査(validation)できるのでやってみます。
インスタンス生成時に 要素数の制限値として min_length
(最小長), max_length
(最大長) を渡せます。
属性として定義しても効果がないっぽいので注意してください。
>>> sl = ScoreListField(min_length=1, max_length=3) >>> sl.run_validation([]) Traceback (most recent call last): File "<console>", line 1, in <module> File "/root/venv/lib/python3.6/site-packages/rest_framework/fields.py", line 524, in run_validation self.run_validators(value) File "/root/venv/lib/python3.6/site-packages/rest_framework/fields.py", line 549, in run_validators raise ValidationError(errors) rest_framework.exceptions.ValidationError: ['Ensure this field has at least 1 elements.'] >>> sl.run_validation([1, 2, 3]) [1, 2, 3] >>> sl.run_validation([1, 2, 101]) Traceback (most recent call last): File "<console>", line 1, in <module> File "/root/venv/lib/python3.6/site-packages/rest_framework/fields.py", line 523, in run_validation value = self.to_internal_value(data) File "/root/venv/lib/python3.6/site-packages/rest_framework/fields.py", line 1629, in to_internal_value return [self.child.run_validation(item) for item in data] File "/root/venv/lib/python3.6/site-packages/rest_framework/fields.py", line 1629, in <listcomp> return [self.child.run_validation(item) for item in data] File "/root/venv/lib/python3.6/site-packages/rest_framework/fields.py", line 524, in run_validation self.run_validators(value) File "/root/venv/lib/python3.6/site-packages/rest_framework/fields.py", line 549, in run_validators raise ValidationError(errors) rest_framework.exceptions.ValidationError: ['Ensure this value is less than or equal to 100.'] >>> sl.run_validation([1, 2, 3, 4]) Traceback (most recent call last): File "<console>", line 1, in <module> File "/root/venv/lib/python3.6/site-packages/rest_framework/fields.py", line 524, in run_validation self.run_validators(value) File "/root/venv/lib/python3.6/site-packages/rest_framework/fields.py", line 549, in run_validators raise ValidationError(errors) rest_framework.exceptions.ValidationError: ['Ensure this field has no more than 3 elements.']
続いて、 DictField。これは 辞書(オブジェクト)型を期待します。
Listと違い min_length
, max_length
でキーの個数は指定できないようです。
今回は child
にシリアライザを指定してみましょう。
from rest_framework import serializers class CommentDictField(serializers.DictField): child = CommentSerializer()
辞書インスタンスを渡します。
>>> cd = CommentDictField() >>> cd.run_validation({ ... 'a': {'email': 'a@example.com', 'content': 'aa', 'created': '2016-01-27T15:17:10.375877'}, ... 'b': {'email': 'b@example.com', 'content': 'bbb', 'created': '2016-01-27T15:17:10.375877'}, ... }) {'a': OrderedDict([ ('email', 'a@example.com'), ('content', 'aa'), ('created', datetime.datetime(2016, 1, 27, 15, 17, 10, 375877, tzinfo=<UTC>))]), 'b': OrderedDict([('email', 'b@example.com'), ('content', 'bbb'), ('created', datetime.datetime(2016, 1, 27, 15, 17, 10, 375877, tzinfo=<UTC>))])} >>> cd.run_validation({ ... 'c': {'email': 'c', 'content': 'foo bar', 'cccc': '2016-01-27T15:17:10.375877'}, ... }) Traceback (most recent call last): File "<console>", line 2, in <module> File "/root/venv/lib/python3.6/site-packages/rest_framework/fields.py", line 523, in run_validation value = self.to_internal_value(data) File "/root/venv/lib/python3.6/site-packages/rest_framework/fields.py", line 1674, in to_internal_value for key, value in data.items() File "/root/venv/lib/python3.6/site-packages/rest_framework/fields.py", line 1674, in <dictcomp> for key, value in data.items() File "/root/venv/lib/python3.6/site-packages/rest_framework/serializers.py", line 435, in run_validation value = self.to_internal_value(data) File "/root/venv/lib/python3.6/site-packages/rest_framework/serializers.py", line 478, in to_internal_value raise ValidationError(errors) rest_framework.exceptions.ValidationError: {'email': ['Enter a valid email address.'], 'created': ['This field is required.']} >>> cd.run_validation({ ... 'd': {'email': 'd@example.com', 'content': 'foo bar', 'cccc': '2016-01-27'}, ... }) Traceback (most recent call last): File "<console>", line 2, in <module> File "/root/venv/lib/python3.6/site-packages/rest_framework/fields.py", line 523, in run_validation value = self.to_internal_value(data) File "/root/venv/lib/python3.6/site-packages/rest_framework/fields.py", line 1674, in to_internal_value for key, value in data.items() File "/root/venv/lib/python3.6/site-packages/rest_framework/fields.py", line 1674, in <dictcomp> for key, value in data.items() File "/root/venv/lib/python3.6/site-packages/rest_framework/serializers.py", line 435, in run_validation value = self.to_internal_value(data) File "/root/venv/lib/python3.6/site-packages/rest_framework/serializers.py", line 478, in to_internal_value raise ValidationError(errors) rest_framework.exceptions.ValidationError: {'created': ['This field is required.']}
- info
上記では継承して新規のフィールドクラスを定義しましたが、
child
引数を渡すことでそのままインスタンスを作成することもできます。定義例
class SomeSerializer(serializers.Serializer): scores = serializers.ListField(child=serializers.IntegerField(max_value=100)) comments = serializers.DictField(child=CommentSerializer())
最後に JSONField
ですが、
これはドキュメントを読んだだけではちょっと使いみちが見いだしにくいフィールドでした。
binary=True
を指定すると、入力値のJSONをデシリアライズしてくれるようです。
マルチパートリクエストのパラメータの中で JSON
を渡してる場合とかで使えそうですね。
それ以外の使いみちはよくわかりませんが。(知ってる人いたら教えて)
>>> j = serializers.JSONField(binary=True) >>> j.run_validation('{"a": 1, "b": 2}') {'a': 1, 'b': 2}
RelatedFields と 複合フィールドを組み合わせるだけで割りと複雑なことにも対応はできると思いますが、 それでも対応できない場合にはカスタムフィールド を定義しましょう。
カスタムフィールド
自分でフィールドを定義したい場合もあるでしょう。
その場合は serializers.Field
を継承したクラスを定義します。
カスタムフィールドには以下のメソッドを定義できます。
- 入力専用
- get_value
- シリアライザが受け取った
data
を第一引数に受けとり、処理した結果を返す。 デフォルトでは入力値の該当フィールドをそのまま返却する。
- シリアライザが受け取った
- to_internal_value
- 入力対象のフィールドを評価する。第一引数で
get_value
メソッドの処理結果を受けとる。
- 入力対象のフィールドを評価する。第一引数で
- get_value
- 出力専用
- get_attribute
- シリアライザが受け取った
instance
を第一引数に受けとり、処理した結果を返す。 - デフォルトでは インスタンスの該当フィールドをそのまま返却する。
- シリアライザが受け取った
- to_representation
- 出力対象のフィールドの評価する。
- 第一引数で
get_attribute
メソッドの処理結果を受けとる。
- get_attribute
というわけで、すでにN番煎じですが、再帰的にリレーションをたどるフィールドを定義してみましょう。これは出力専用の機能ですね。
ツリー構造をシリアライズするフィールドを考えてみます。
class Tree(object): def __init__(self, value, parent=None): self.value = value self.parent = parent ta = Tree('a') tb = Tree('b', ta) tc = Tree('c', ta) td = Tree('d', tc) te = Tree('e', td) tf = Tree('f', te)
class RecursiveField(serializers.Field): def to_representation(self, obj): _obj = TreeSerializer(obj).data if obj.parent: _obj['parent'] = TreeSerializer(obj.parent).data return _obj class TreeSerializer(serializers.Serializer): parent = RecursiveField() value = serializers.CharField(default='test')
これを使い tf
から辿れる 値すべてを表示してみます。
(改行とかインデントは私が手動でやりました)
>>> TreeSerializer(tf).data { 'value': 'f', 'parent': { 'value': 'e', 'parent': { 'value': 'd', 'parent': { 'value': 'c', 'parent': { 'value': 'a', 'parent': None } } } } }
一応 再帰構造 をシリアライズできました。ちなみに tb
は
tf
から辿れないので表示されないのは期待通りです。
でも、RecursiveField と Serializer が相互に参照しあってるし、 二回呼び出してるし、フィールド名が固定になっちゃうし、気持ち悪いですね。
どうやら、 self.parent
を使うと期待通りの動作をするんじゃないかとのこと。
Representing hierarchical data with django-rest-frameworkhttp://voorloopnul.com/blog/representing-hierarchical-data-with-django-rest-framework/
やってみよう。
class RecursiveField(serializers.Field): def to_representation(self, obj): return self.parent.__class__(obj, context=self.context).data class TreeSerializer(serializers.Serializer): parent = RecursiveField() value = serializers.CharField(default='test')
だいぶシンプルになりました。
>>> TreeSerializer(tf).data { 'value': 'f', 'parent': { 'value': 'e', 'parent': { 'value': 'd', 'parent': { 'value': 'c', 'parent': { 'value': 'a', 'parent': None } } } } }
結果も同じになりましたね!
さて、もし参照が循環していたらどうなるでしょうか
from rest_framework import serializers class RecursiveField(serializers.Field): def to_representation(self, obj): return self.parent.__class__(obj, context=self.context).data class TreeSerializer(serializers.Serializer): parent = RecursiveField() value = serializers.CharField(default='test') class Tree(object): def __init__(self, value, parent=None): self.value = value self.parent = parent ta = Tree('a') tb = Tree('b', ta) tc = Tree('c', tb) ta.parent = tc
ta
の親に tc
をもってきて、ループするようにしてみました。
>>> TreeSerializer(tc).data RecursionError: maximum recursion depth exceeded while calling a Python object
無限ループって怖くね?っていうエラーですね
これは私が用意した簡易的なツリー構造ですが、実際の案件でもレコードの外部参照が循環することは十分にありえます。
本来は登録の段階でこのようなデータが入らないように考慮するべきでしょうが、入ってしまった場合のことも考慮して、
シリアライザ作成時に重複チェック用の set
を使い検査するように改修してみます。
from rest_framework import serializers class RecursiveField(serializers.Field): def to_representation(self, obj): parent = self.parent.__class__(obj, processed=self.parent.processed) if obj.value in self.parent.processed: return self.parent.processed.add(obj.value) return parent.data class TreeSerializer(serializers.Serializer): parent = RecursiveField() value = serializers.CharField(default='test') def __init__(self, *args, **kwargs): processed = kwargs.pop('processed', set()) super().__init__(*args, **kwargs) self.processed = processed class Tree(object): def __init__(self, value, parent=None): self.value = value self.parent = parent ta = Tree('a') tb = Tree('b', ta) tc = Tree('c', tb) ta.parent = tc
>>> TreeSerializer(tc, processed=set()).data { 'value': 'c', 'parent': { 'value': 'b', 'parent': { 'value': 'a', 'parent': { 'value': 'c', 'parent': None } } } }
エラーにならず結果を得ることができました。 (ちなみに processed引数がない場合は自動生成されるので、指定しなくてもいいです)
重複時が見つかったときにリターンしてるので一つ分は重複してしまいますが、致し方ないですね。
- info
- Django model でやる場合は
obj.value
をobj.id
かobj
に直せばうまくいくはずです。
- Django model でやる場合は
バリデーション
バリデータは 入力値のチェックおよび整形をするためのクラスまたは関数です。 バリデータはフィールドごとに複数設定できます。複数のフィールドにまたがるものは Meta に 書くこともできます。
詳しくはバリデータの公式ドキュメントを参照ください。
ここでは使用頻度が高そうな UniqueValidator と、カスタムバリデータの定義方法についてだけ解説します。
validators.UniqueValidator
from django.contrib.auth.models import User from rest_framework import serializers, validators class UserSerializer(serializers.ModelSerializer): email = serializers.EmailField(validators=[validators.UniqueValidator(queryset=User.objects.all(), message='重複!')]) class Meta: model = User fields = ('username', 'email')
こんな感じで定義します。
queryset
引数だけが必須です。今回は EmailField が必須な感じにしてみました。
>>> serializer = UserSerializer(data={'username': 'haru', 'email': 'test@example.com'}) >>> serializer.is_valid() True >>> serializer.save() <User: haru> >>> serializer2 = UserSerializer(data={'username': 'haro', 'email': 'test@example.com'}) >>> serializer2.is_valid() False >>> serializer2.errors {'email': ['重複!']}
使うとこのようになります。簡単ですね。バリデーションエラーメッセージは errors 属性に入ってます。
カスタムバリデータ
カスタムバリデータは呼び出し可能オブジェクトなものと、シリアライザに紐づくメソッド型のものがあります。
関数形式のバリデータ:
ちょっと仰々しい言い方になってしまいましたが、もっとも単純なものは関数です。
これは本当にシンプルで、バリデーション対象の値を一つ受取り、期待通りでなければ ValidationError 例外を発生させるだけです。
たとえばこんな感じで定義し、使うときは さっきと同じように validators 引数に与えるだけです。
def even_number(value): if value % 2 != 0: raise serializers.ValidationError('偶数じゃないぜ!')
これで事足りる場合は関数で作成することをオススメします。
クラス形式のバリデータ:
含みのある言い方をしましたが、実はクラスを使って書くこともできます。
クラスベースのバリデータとは要は __call__
が定義されていて呼び出し可能になっているクラスのインスタンスです。
ファクトリ関数を定義しなくても処理に状態を持てるのはやはりよいですね。
語尾に任意の文字列を含んでいないかを確認するバリデータを実装してみましょう。
class SuffixValidator(object): def __init__(self, suffix, message='Suffix of the string is something wrong!'): self.suffix = suffix self.message = message def __call__(self, value): if value.endswith(self.suffix): raise serializers.ValidationError(self.message)
たとえば、入力文字の語尾に出現する謎の文字列を弾くようなシリアライザは上記を用いて以下のようにかけます。
>>> class BetoSerializer(serializers.Serializer): ... word = serializers.CharField(validators=[SuffixValidator('ベト', message='あぶないひとです!')]) ... >>> s1 = BetoSerializer(data={'word': 'おはよう'}) >>> s1.is_valid() True >>> s2 = BetoSerializer(data={'word': 'なんか疲れたベト'}) >>> s2.is_valid() False >>> s2.errors {'word': ['あぶないひとです!']}
おまわりさんこのひとです!
なんか疲れたベト
— tell-k (@tell_k) July 22, 2016
メソッド形式のバリデータ:
最後にメソッド型です。みなさんは Form を使ったことがあるでしょうか。 (参考: Djangoのフォームまとめー)
Form でバリデーションを書くとき clean_<field_name>
とか clean
いうメソッドを定義すると自動的に呼び出されるんでしたね。
それと同じように validate_<field_name>
とか validate
っていうメソッドを定義するだけです。
それぞれフィールドごとのバリデーション、フィールドを跨ぐバリデーションを担当します。フォームと同じなのでとっても簡単ですね。
メソッド型のバリデータは self
(メソッドの第一引数) を通じて シリアライザのインスタンス変数にアクセスできるという大きなアドバンテージがあります。
シリアライザの context
引数に request オブジェクトを指定しておけば、
セッション情報にアクセスしてログイン中のユーザを参照するようなバリデータも書けるのですね。
これによって「ログイン中のユーザの購入履歴に含まれている商品」みたいなバリデータもかけちゃうわけです。すごーい。
ちなみにこの段階ではバリデーションが終わってないので validated_data
へのアクセスは不可です。
- How to get Request.User in Django-Rest-Framework serializer?
- Pass request context to serializer from Viewset in Django Rest Framework
- Including extra context
- info
一応、
request
を取得する実装例だけ示しておきます。view から
request
を渡す際に 愚直に Serializerインスタンス取得時に context 引数を指定する方法でももちろんいいんですが、self.get_serializer
で シリアライザを取得した場合はデフォルトでself.context
からrequest
が抽出できます。- views.MyUserViewSet
- serializers.UserSerializer
class MyUserViewSet(ModelViewSet): serializer_class = UserSerializer queryset = User.objects.filter() def update(self, request, pk): # context 引数を指定する例 instance = instance = self.get_object() serializer = UserSerializer(instance, context={'request': request}) # なにかする def create(self, request): # get_serializer を使う例 serializer = self.get_serializer(data=request.data) # なにかする
class UserSerializer(serializers.ModelSerializer): def validate_something(self, value): request = self.context['request'] # do something using request
context
とは request
のように利用者や状況によって変わるような流動的なデータを指すことが多いようです。
適当に動作を検証してみましょう。今回は、コンソールで確認できるものにしたいので、
request
にアクセスするような 例はやりません。
そろそろネタが尽きたので適当にお昼ご飯を評価するシリアライザです。
from rest_framework import serializers class OhiruSerializer(serializers.Serializer): price = serializers.IntegerField() evaluation = serializers.ChoiceField(['まずい', 'ふつう', 'おいしい', 'めちゃうま']) def validate_price(self, value): # print('price', value) if value > 2000: raise serializers.ValidationError('そもそもたかすぎー') return value def validate(self, data): # print('data', data) if self.context.get('hungry'): return data if data['evaluation'] == 'まずい' and data['price'] > 500: raise serializers.ValidationError('こんなまずい飯に500円以上払えるか!') if data['evaluation'] == 'ふつう' and data['price'] > 1000: raise serializers.ValidationError('値段の割に普通だよねー') if data['evaluation'] == 'おいしい' and data['price'] > 1500: raise serializers.ValidationError('たしかにおいしいけど 1500 円以上払うほどではないかな..') return data
値段と味によってお昼ご飯を上から目線でバリデーションします。
context
には空腹(hungry)フラグを指定でき、空腹の場合はバリデータが機能しません。
# [OK] まずくても安ければ許される。そんな世界 >>> s1 = OhiruSerializer(data={'price': 300, 'evaluation': 'まずい'}) >>> s1.is_valid() True # [OK] 安くても美味しいご飯はある。 >>> s2 = OhiruSerializer(data={'price': 450, 'evaluation': 'ふつう'}) >>> s2.is_valid() True # [NG] 600円でまずいともう行きたくない >>> s3 = OhiruSerializer(data={'price': 600, 'evaluation': 'まずい'}) >>> s3.is_valid() False >>> s3.errors {'non_field_errors': ['こんなまずい飯に500円以上払えるか!']} # [OK] 900円ならいいかな >>> s4 = OhiruSerializer(data={'price': 900, 'evaluation': 'ふつう'}) >>> s4.is_valid() True # [NG] 1200円ならおいしくないとだめ >>> s5 = OhiruSerializer(data={'price': 1200, 'evaluation': 'ふつう'}) >>> s5.is_valid() False >>> s5.errors {'non_field_errors': ['値段の割に普通だよねー']} # [OK] おいしければ 1500 円まで出す >>> s6 = OhiruSerializer(data={'price': 1500, 'evaluation': 'おいしい'}) >>> s6.is_valid() True # [NG] すこしおいしいくらいじゃ 1800 円の投資は回収できません >>> s7 = OhiruSerializer(data={'price': 1800, 'evaluation': 'おいしい'}) >>> s7.is_valid() False >>> s7.errors {'non_field_errors': ['たしかにおいしいけど 1500 円以上払うほどではないかな..']} # [OK] めっちゃおいしければ 1800 円も可 >>> s8 = OhiruSerializer(data={'price': 1800, 'evaluation': 'めちゃうま'}) >>> s8.is_valid() True # [NG] どんなにおいしくても 2000 円以上は絶対に払わない >>> s9 = OhiruSerializer(data={'price': 2500, 'evaluation': 'めちゃうま'}) >>> s9.is_valid() False >>> s9.errors {'price': ['そもそもたかすぎー']} # [NG] ごみのような飯でしたが、そもそもそんな評価はなかった >>> s0 = OhiruSerializer(data={'price': 100, 'evaluation': 'ごみ'}) >>> s0.is_valid() False >>> s0.errors {'evaluation': ['"ごみ" is not a valid choice.']} # [OK] 空腹は最高の調味料ということで.. >>> おなかすいてる = OhiruSerializer(data={'price': 1200, 'evaluation': 'まずい'}, context={'hungry': True}) >>> おなかすいてる.is_valid() True
- warning
- メソッド型の バリデータにはいくつかの注意点があります。
validate
はvalidate_<field_name>
のバリデーション結果すべてに問題がない場合にのみ実行される。- よって s9, s0 のバリデーションは
validate
メソッドに到達していない。
- よって s9, s0 のバリデーションは
- 値を返却する必要がある
validate
は 辞書形式のオブジェクトを返却する必要がある。 通常は data をそのまま返却すれば良い。validate_<field_name>
はフィールドの値を返却する必要がある。返却しなくてもエラーにはならないが、 その場合 validated_data のフィールド値はNone
になる。通常はvalue
(第一引数)をそのまま返却すれば良い。
- メソッド型の バリデータにはいくつかの注意点があります。
というわけでこの記事が役に立った方はめっちゃ美味しいご飯をおごってください。