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

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

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

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

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

Note

ビュー については View の使い方をまとめてみた を参照してください。

初めて DRF を触る方は 先にビュー編を見ることをオススメします。 シリアライザ単体で使うことはまずないと思うので。

この記事の先頭に書いてあったインストールの手順とかはそちらの記事に移動しました

基本的に情報は v3.7.7 時点の 公式ドキュメント を参考にしました。 一部以下のブログも参照させてもらいました。

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

7時半から空手の稽古があるからあとで読む? 今日は休め

目次

What is serializer

Serializer は データ の入出力を扱い、モデルへの橋渡しをするクラスです。

(プレーンな)Djangoには Form クラスがありましたが、あれの API用 がシリアライザと考えればよいでしょう。

実は API を扱う場合 Django Form はあまり適していません。

x-www-form-urlencoded 形式での入力を前提としているFormでは JSON や XMLのような複雑なデータは処理できません。

また、 大抵は レスポンスも JSON などで返却するため、Form オブジェクト のHTML レンダリング もあまり使われません。

Serializer は

  • 複雑な入力値をモデルに合わせてバリデーションしてレコードに伝えたり(入力)

  • Model(レコード)を適切な形式にフォーマットしたり(出力)

と言った具合に、 APIの リクエスト / レスポンス に特化した機能を提供します。

Note

  • Serializer では 入力専用, 出力専用, または いずれ にも機能するものがあります。

  • この記事では いずれにも機能するもの を除き、できるだけ明記していきます。

  • 入力=リクエスト, 出力=レスポンス の方向ですのでご了承ください。

Serializer types

Serializer にはいくつか種類があります。

serializers.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と同じですね。

続いて、 create と update メソッドです。 保存時の挙動というのは .save() メソッド の呼び出しを意味しています。これはお察しの通り入力専用の機能です。

たとえば 以下のような comment オブジェクトが存在したとして

comment = Comment(email='leila@example.com', content='fooooo')

これを第一引数 に与えるか否かによって、serializer の動作が変わります。

# .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 という引数が存在し、 シリアライザに渡された data 引数(第二引数) の内、バリデーションを通過した値が格納されています。

さて、この save() ですが、以下のような注意点があります。

  • is_valid() をコールした後にしか呼べない。これは is_valid() メソッドによって validated_data が作成されるからです。 これらは共に 入力専用の属性です。

    >>> serializer = CommentSerializer(comment, {'email': 'leila@example.com', 'content': 'foo bar', 'created': '2016-01-27T15:17:10.375877'})
    >>> 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 180, in save
        'You must call `.is_valid()` before calling `.save()`.'
    AssertionError: You must call `.is_valid()` before calling `.save()`.
    
  • data (出力専用)属性を参照した後にはコールできない。おそらく 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.
    

Note

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.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')
        extra_kwargs = {
            'username': {'read_only': True},
            '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

フィールドの制限設定には read_only_fields, write_only_fields がありますが、これらの利用は 非推奨になり、これからは extra_kwargs を使うらしいです。

extra_kwargs辞書型 で フィールドをキーに取ります。

  • 出力専用フィールド の場合には 'read_only': True

    • 本来のモデルとしては指定必須で、読み出しもしたいけど、Serializerのバリデーションにはかけたくないような場合に指定します。

  • 入力専用フィールド の場合には 'write_only': True

    • Serializer から値を入れたいけど、読み出しはしたくない場合に指定します。

    • すぐに思い浮かぶのはパスワードとかEmailアドレスなどの個人情報関連のデータですね。

を指定します

実際に入力値を使ってレコードを作ってみましょう。

>>> 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 を使って定義した方が良いかもしれません。

serializers.ListSerializer

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

概ね、前述したように many=True を指定した Serializer クラスと同様です。

# さっきの続きから

class UsersSerializer(serializers.ListSerializer):
    child = UserSerializer()

    full_name = serializers.SerializerMethodField()

    def get_full_name(self, instance):
        return instance.get_full_name()  # User に元からあるメソッドを呼び出してるだけ

    class Meta:
        model = User
        fields = ('username', 'email', 'first_name', 'last_name', 'full_name')
        extra_kwargs = {
            'username': {'read_only': True},
            'password': {'write_only': True},
        }

    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)

今回の create は バルクインサートにしてみました。

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'

Fields

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

公式フィールドリファレンス を見たほうが早いかもしれませんが、重要なものは解説しておきましょう。

Common args

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

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

read_only
  • 出力専用のフィールドか否かを Boolean で指定します。

  • extra_kwargs でも指定できます。

write_only
  • 入力専用のフィールドか否かを Boolean で指定します。

  • extra_kwargs でも指定できます。

required
  • 値の指定が必須かどうかを Boolean で指定します。

  • デフォルトは True ですが、 False を指定すると入力値に該当フィールドがなくてもスルーしてくれます。

  • これは入力専用の設定です。

default
  • 値が省略された場合に補完する値を指定します。

    • オブジェクトがコーラブルでない場合はその値がデフォルト値になります。

    • オブジェクトがコーラブルな場合は引数を与えずにそのオブジェクトをコールし、その戻り値がデフォルト値になります。

      • オブジェクトに set_context メソッドが定義されている場合、そのフィールドを第一引数に指定して メソッドを呼び出します。

  • この引数が指定された場合 required は 自動的に False になります。

    • これらを共に有効にするとエラーが発生します。

  • これは partial_update のときには機能しません。

  • ちなみに default によって設定された値はバリデーションの対象にはならないので注意が必要です。

source

通常、シリアライザの属性名は対象のオブジェクトの属性名と一致しますが、合わせられないこともあります。そんなときはこの引数で別の属性名を指定してあげましょう。

validators

バリデータをリスト形式で指定します。詳しくは Validations を参照ください。

バリデータは後述するのでそれ以外について動作を確認してみましょう。

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'}

Miscellaneous fields

直訳すると雑多なフィールドということで、おそらく分類できないからこういう名前になったんでしょう。 以下のフィールドについて解説します。

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 フィールドは無視されて 顔色だけが出力されていますね。

Composite fields

直訳すると 複合フィールド ということで、複数のフィールドをまとめたフィールドのことです。

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.']}

最後に JSONField ですが、これはドキュメントを読んだだけではちょっと使いみちが見いだしにくいフィールドでした。

binary=True を指定すると、入力値のJSONをデシリアライズしてくれるようです。マルチパートリクエストのパラメータの中で JSON を渡してる場合とかで使えそうですね。

それ以外の使いみちはよくわかりませんが。(知ってる人いたら教えて)

>>> j = serializers.JSONField(binary=True)
>>> j.run_validation('{"a": 1, "b": 2}')
{'a': 1, 'b': 2}

RelatedFields と 複合フィールドを組み合わせるだけで 割りと複雑なことにも対応はできると思いますが、それでも対応できない場合には カスタムフィールド を定義しましょう。

Custom fields

自分でフィールドを定義したい場合もあるでしょう。その場合は 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 が相互に参照しあってるし、二回呼び出してるし、フィールド名が固定になっちゃうし、気持ち悪いですね。

どうやら、 representing hierarchical data with django-rest-framework の記事によると self.parent を使うと期待通りの動作をするんじゃないかとのこと。 やってみよう。

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 引数がない場合は自動生成されるので、指定しなくてもいいです

重複時が見つかったときにリターンしてるので一つ分は重複してしまいますが、致し方ないですね。

Note

Django model でやる場合は obj.valueobj.idobj に直せばうまくいくはずです。

Validation

バリデータは 入力値のチェックおよび整形をするためのクラスまたは関数です。バリデータはフィールドごとに複数設定できます。複数のフィールドにまたがるものは 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 属性に入ってます。

Custom Validator

カスタムバリデータは 呼び出し可能オブジェクトなものと、シリアライザに紐づくメソッド型のものがあります。

関数形式のバリデータ function-validator function-validator

ちょっと仰々しい言い方になってしまいましたが、もっとも単純なものは関数です。

これは本当にシンプルで、バリデーション対象の値を一つ受取り、期待通りでなければ ValidationError 例外を発生させるだけです。

たとえばこんな感じで定義し、使うときは さっきと同じように validators 引数に与えるだけです。

def even_number(value):
    if value % 2 != 0:
        raise serializers.ValidationError('偶数じゃないぜ!')

これで事足りる場合は関数で作成することをオススメします。

クラス形式のバリデータ class-validator class-validator

含みのある言い方をしましたが、実はクラスを使って書くこともできます。 クラスベースのバリデータとは要は __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': ['あぶないひとです!']}

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

メソッド形式のバリデータ method-validator method-validator

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

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

メソッド型のバリデータは self (メソッドの第一引数) を通じて シリアライザの インスタンス変数にアクセスできるという大きなアドバンテージがあります。 シリアライザの context 引数に request オブジェクトを指定しておけば、セッション情報にアクセスしてログイン中のユーザを参照するようなバリデータも書けるのですね。

これによって「ログイン中のユーザの購入履歴に含まれている商品」みたいなバリデータもかけちゃうわけです。すごーい。 ちなみにこの段階ではバリデーションが終わってないので validated_data へのアクセスは不可です。

Note

一応、 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 (第一引数)をそのまま返却すれば良い。

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