2021-03-13

[Django REST Framework] Serializer の 使い方 をまとめてみた

DjangoRESTFramework (以降 DRF という) を最近良く使っているのですが 設定項目が多すぎて情報探すのに時間がかかっちゃうので、自分なりにまとめてみました。

2月の後半くらいに書いてたんですが、ブログの改修に時間がかかりすぎて公開が遅れたのは内緒。

個人的な感覚ですが、このライブラリの機能を大きく分けると Serializer, View に分かれます。

本当は全部通しで書きたかったんですが、長くなりすぎたので View の部分は別の記事に分割します。

info

自分で言うのも何ですが分割しても尚長いです。クオリティに自信はありませんが、長さには自信があります。

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 にはいくつか種類があります。

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では永続化はされません)

このメソッドにより先程シリアライザに定義した createupdate メソッドが 状況に応じて 透過的に呼び出されます。 それぞれ新規作成・更新するためのメソッドで、お察しの通り入力専用の機能です。

>>> 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 を上書きし、 createupdate メソッドに渡っていきます。 (該当コード)

    >>> 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_onlyread_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

serializers.ListSerializer

ListSerializer は 複数のモデルを扱うことを前提にしたシリアライザです。

前述した many=True を引数に指定した Serializer インスタンスと似たような動きをします。 複数受け取ることが前提なので、インスタンス化の際に many=True を指定する必要がありません。

warning
  • many=True のときとは違い、 create, update メソッドで受け取る validated_data も複数形(list型)になります。

createupdate メソッドを省略した場合は 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'

フィールド

シリアライザに指定できるフィールドについて解説します。 CharField や DateField などの基本的なフィールドは Form とかモデルと同じなので除外です。

Serializer fields を見たほうが早いかもしれませんが、重要なものは解説しておきましょう。

共通する引数

詳細は Core arguments のリンクを参照してほしいのですが、 Serializer の Field には共通して指定できる引数というものが幾つかあります。

ここでは重要なものに絞って説明します。

read_only
  • 出力専用のフィールドか否かを Boolean で指定します。 (デフォルトは False)
write_only
  • 入力専用のフィールドか否かを Boolean で指定します。 (デフォルトは False)
    • extra_kwargs でも指定できます。
required
  • 値の指定が必須かどうかを Boolean で指定します。
    • デフォルトは True ですが、 False を指定すると入力値に該当フィールドがなくてもスルーしてくれます。
    • これは入力専用の設定です。
allow_null
  • None の入力を許容する場合は True を指定します。 (デフォルトは False です)

    • このオプションを有効にすると 次に説明する default オプションは None になりますが、デシリアライズのデフォルト値が null になるわけではありません。
    • 何の役に立つかといえば、実は patch リクエストを多用しているようなアプリケーションでは重宝します。
  • 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 で受けてほしいんですがどうしたらいいんでしょうか。

答えはスタックオーバーフローにありました。 DRF: Simple foreign key assignment with nested serializers?With Django REST Framework, a standard ModelSerializer will allow ForeignKey model relationships to be assigned or changed by POSTing an ID as an Integer. What's the simplest way to get this behav...https://stackoverflow.com/questions/29950956/drf-simple-foreign-key-assignment-with-nested-serializers

別のフィールドを定義して、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_attribute
    • シリアライザが受け取った instance を第一引数に受けとり、処理した結果を返す。
    • デフォルトでは インスタンスの該当フィールドをそのまま返却する。
  • to_representation
    • 出力対象のフィールドの評価する。
    • 第一引数で 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 } } } } }

一応 再帰構造 をシリアライズできました。ちなみに tbtf から辿れないので表示されないのは期待通りです。

でも、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.valueobj.idobj に直せばうまくいくはずです。

バリデーション

バリデータは 入力値のチェックおよび整形をするためのクラスまたは関数です。 バリデータはフィールドごとに複数設定できます。複数のフィールドにまたがるものは 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': ['あぶないひとです!']}

おまわりさんこのひとです!

メソッド形式のバリデータ:

最後にメソッド型です。みなさんは Form を使ったことがあるでしょうか。 (参考: Djangoのフォームまとめー)

Form でバリデーションを書くとき clean_<field_name> とか clean いうメソッドを定義すると自動的に呼び出されるんでしたね。 それと同じように validate_<field_name> とか validate っていうメソッドを定義するだけです。 それぞれフィールドごとのバリデーション、フィールドを跨ぐバリデーションを担当します。フォームと同じなのでとっても簡単ですね。

メソッド型のバリデータは self (メソッドの第一引数) を通じて シリアライザのインスタンス変数にアクセスできるという大きなアドバンテージがあります。

シリアライザの context 引数に request オブジェクトを指定しておけば、 セッション情報にアクセスしてログイン中のユーザを参照するようなバリデータも書けるのですね。

これによって「ログイン中のユーザの購入履歴に含まれている商品」みたいなバリデータもかけちゃうわけです。すごーい。

ちなみにこの段階ではバリデーションが終わってないので validated_data へのアクセスは不可です。

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
  • メソッド型の バリデータにはいくつかの注意点があります。
    • validatevalidate_<field_name> のバリデーション結果すべてに問題がない場合にのみ実行される。
      • よって s9, s0 のバリデーションは validate メソッドに到達していない。
    • 値を返却する必要がある
      • validate は 辞書形式のオブジェクトを返却する必要がある。 通常は data をそのまま返却すれば良い。
      • validate_<field_name> はフィールドの値を返却する必要がある。返却しなくてもエラーにはならないが、 その場合 validated_data のフィールド値は None になる。通常は value (第一引数)をそのまま返却すれば良い。

というわけでこの記事が役に立った方はめっちゃ美味しいご飯をおごってください。