Help us understand the problem. What is going on with this article?

Django Serializerの中身

DjangoのSerializerでいつ何がどう呼ばれてどのmethodをどう実装すればよいかがわからないことがあります。
公式ドキュメントを読んでもいまいちどう実装すればいいかわかないという人(自分も具体的な使い方が想像できませんでした。)も多いと思うのでよく使う以下の3つのメソッド/プロパティについて中のModelSerializerクラスのソースを実際に読みながらまとめてみました。
- save
- is_valid
- data

serializer.save()

まずはObject作成時や更新時に呼ぶメソッドの解説をします。
一部省略していますが、saveは以下のように実装されています。

serializers.py
    def save(self, **kwargs):
        # 一部省略
        # ①
        assert hasattr(self, '_errors'), (
            'You must call `.is_valid()` before calling `.save()`.'
        )

        assert not self.errors, (
            'You cannot call `.save()` on a serializer with invalid data.'
        )

        # Guard against incorrect use of `serializer.save(commit=False)`
        assert 'commit' not in kwargs, (
            "'commit' is not a valid keyword argument to the 'save()' method. "
            "If you need to access data before committing to the database then "
            "inspect 'serializer.validated_data' instead. "
            "You can also pass additional keyword arguments to 'save()' if you "
            "need to set extra attributes on the saved model instance. "
            "For example: 'serializer.save(owner=request.user)'.'"
        )

        assert not hasattr(self, '_data'), (
            "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. "
        )

        # ②
        validated_data = dict(
            list(self.validated_data.items()) + list(kwargs.items())
        )

        # ③
        if self.instance is not None:
            self.instance = self.update(self.instance, validated_data)
            assert self.instance is not None, (
                '`update()` did not return an object instance.'
            )
        else:
            self.instance = self.create(validated_data)
            assert self.instance is not None, (
                '`create()` did not return an object instance.'
            )

        return self.instance

①の解説

①では属性のチェックをしています。
上から順に、assert hasattr(self, '_errors')...ではインスタンスに_errors属性が生成されているかをチェックしています。
これは後で確認しますが、is_validを実行すると作成されます。

続いてassert not self.errors...では、errorsが空であるかをチェックしています。これもis_validで生成されます。
errorsがある場合はそこでエラーとなります。

3つ目のassert 'commit' not in kwargs...ではcommitというキーで引数を渡していないかをチェックしています。
commitというkeyは利用できないようですね。

4つ目のassert not hasattr(self, '_data')...は、_dataという属性を持っていないかをチェックしています。
これも後で確認しますが、dataプロパティを参照したときに生成されるオブジェクトです。
保存前にdataプロパティを参照することはできません。

②の解説

引数で渡したkwargsvalidated_dataを結合して新たなvalidated_dataを生成しています。
kwargsは引数で渡したものそのものなので、そのままです。
ではvalidated_dataはどこで生成されてるかというと、これもis_validで生成される_validated_dataを参照するプロパティです。

serializers.py
    @property
    def validated_data(self):
        if not hasattr(self, '_validated_data'):
            msg = 'You must call `.is_valid()` before accessing `.validated_data`.'
            raise AssertionError(msg)
        return self._validated_data

③の解説

instanceがあるかないかでupdatecreateを呼ぶかが切り替わります。
instanceが生成されるかどうかは初期化時に引数を渡すかどうかで決まります。
以下が__init__の中身です。

serializers.py
    def __init__(self, instance=None, data=empty, **kwargs):
        self.instance = instance
        if data is not empty:
            self.initial_data = data
        self.partial = kwargs.pop('partial', False)
        self._context = kwargs.pop('context', {})
        kwargs.pop('many', None)
        super(BaseSerializer, self).__init__(**kwargs)

Serializerクラスではこのupdatecreateはabstractmethodでオーバーライドせずに呼び出した場合、NotImplementedErrorとなりますが
ModelSerializerクラスではMetaクラスよりデフォルトでupdateやcreateが実装されています。

serializer.is_valid()

続いて先程のsaveでも多く登場したis_validを解説します。
ソースは以下のようになっています。

serializers.py
    def is_valid(self, raise_exception=False):
        # 省略
        # ①
        assert hasattr(self, 'initial_data'), (
            'Cannot call `.is_valid()` as no `data=` keyword argument was '
            'passed when instantiating the serializer instance.'
        )

        # ②
        if not hasattr(self, '_validated_data'):
            try:
                self._validated_data = self.run_validation(self.initial_data)
            except ValidationError as exc:
                self._validated_data = {}
                self._errors = exc.detail
            else:
                self._errors = {}

        # ③
        if self._errors and raise_exception:
            raise ValidationError(self.errors)

        return not bool(self._errors)

①の解説

assert hasattr(self, 'initial_data')...の箇所ですが、これは先程の__init__で確認できます。data=emptyでdefaultの値を入れ
if data is not empty:で判定しているのでインスタンス作成時にdataを引数で渡していない場合にエラーになります。

②の解説

if not hasattr(self, '_validated_data'):はすでにis_validが呼ばれているかどうかの判定です。2回目以降が呼ばれても処理結果は1回目のものを返すだけです。
ここの処理自体は、エラーがあれば、self._errorsを詰めるということをしているだけですが、
その前のrun_validationで何をしているかを見てみます。

serializers.py
    def run_validation(self, data=empty):
        """
        We override the default `run_validation`, because the validation
        performed by validators and the `.validate()` method should
        be coerced into an error dictionary with a 'non_fields_error' key.
        """
        (is_empty_value, data) = self.validate_empty_values(data)
        if is_empty_value:
            return data

        value = self.to_internal_value(data)
        try:
            self.run_validators(value)
            value = self.validate(value)
            assert value is not None, '.validate() should return the validated data'
        except (ValidationError, DjangoValidationError) as exc:
            raise ValidationError(detail=as_serializer_error(exc))

        return value

いろいろ処理していますが、最終的な戻り値は、value = self.validate(value)です。
これが最終的にself._validation_dataに代入されます。
validateはデフォルトでは引数をそのままリターンするだけのメソッドです。

serializers.py
    def validate(self, attrs):
        return attrs

validateをオーバーライドすることで独自のvalidationとvalidate_dataの生成が可能になります。

serializer.data

dataの中身は以下のようになっています。

serializers.py
    @property
    def data(self):
        ret = super(Serializer, self).data
        return ReturnDict(ret, serializer=self)

スーパークラスのSerializerクラスを参照しているのでそちらを見ると

serializers.py
    @property
    def data(self):
        # ①
        if hasattr(self, 'initial_data') and not hasattr(self, '_validated_data'):
            msg = (
                'When a serializer is passed a `data` keyword argument you '
                'must call `.is_valid()` before attempting to access the '
                'serialized `.data` representation.\n'
                'You should either call `.is_valid()` first, '
                'or access `.initial_data` instead.'
            )
            raise AssertionError(msg)
        #  ②
        if not hasattr(self, '_data'):
            if self.instance is not None and not getattr(self, '_errors', None):
                self._data = self.to_representation(self.instance)
            elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None):
                self._data = self.to_representation(self.validated_data)
            else:
                self._data = self.get_initial()
        return self._data

①の解説

initial_dataが含まれる、かつ_validated_dataが含まれないということですが
initial_dataは初期化時に、_validated_dataModelSerializerではis_validを読んだときに生成されるデータでした。
つまりこのif文を別の言い方にすると初期データを渡した場合は、is_validを呼ばないとdataプロパティにはアクセスできないということになります。
実際のエラー文言も翻訳すると

シリアライザーにデータキーワード引数が渡されると、シリアル化された `.data`表現にアクセスする前に` .is_valid() `を呼び出す必要があります。
最初に `.is_valid()`を呼び出すか、代わりに `.initial_data`にアクセスする必要があります。

initial_dataならアクセスしてもいいということです。

②の解説

一番外側ののif文は、初回呼ばれたときのみ_dataを生成するロジックを呼ぶようにしている分岐ですね。2回目以降は、初回に生成された_dataを参照するようになっています。

内側の3つの分岐も見ていきましょう。
if self.instance is not None and not getattr(self, '_errors', None):の分岐は、instanceがあり、_errorsがない場合に実行されます。
self.instance is not Noneを満たすためには、初期化時にinstanceを引数で渡すか、saveメソッドを実行する必要があります。
not getattr(self, '_errors', None)は、is_validでエラーが発生していない、あるいはis_validが呼ばれていないことになります。 ただis_validが呼ばれていない場合は、そもそも①の分岐で弾かれるのでis_validでエラーが発生していないという条件だけが残ります。
このinstanceを引数にself.to_representation(self.instance)の戻り値を最終的にリターンします。

次の分岐elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None):は後半部分は先ほどと同様でis_validでエラーがないことが条件になります。
1つ目の条件の逆も条件に加わるので、self.instance is Noneという異なります。instanceは初期化時とsave時に生成・更新されるので初期化時にinstanceを引数に渡さず、saveを呼んでいない場合にこの分岐に入ります。
このinstanceを引数にself.to_representation(self.instance)の戻り値を最終的にリターンします。

最後のelseですが、これはシンプルにis_valiudでエラーが詰められた場合に通ります。

つまりdataプロパティを呼ぶには、is_validを呼んでいる必要があり、
エラーがないかつ、初期化時にself.instanceに詰められる第一引数を与えた場合、またはsaveメソッドを呼んだ場合は、1つ目の分岐に、
エラーがないかつ、初期化時にself.instanceに詰められる第一引数を与えず、saveメソッドを呼んでいない場合は、2つ目の分岐に、
is_validでエラーが発生していた場合には、3つ目の分岐に入ることになります。

さて1つ目の分岐と2つ目の分岐で共通して呼ばれているself.to_representationを見てみましょう。

serializers.py
    def to_representation(self, instance):
        """
        Object instance -> Dict of primitive datatypes.
        """
        ret = OrderedDict()
        fields = self._readable_fields

        for field in fields:
            try:
                attribute = field.get_attribute(instance)
            except SkipField:
                continue

            # We skip `to_representation` for `None` values so that fields do
            # not have to explicitly deal with that case.
            #
            # For related fields with `use_pk_only_optimization` we need to
            # resolve the pk value.
            check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
            if check_for_none is None:
                ret[field.field_name] = None
            else:
                ret[field.field_name] = field.to_representation(attribute)

        return ret

self._readable_fieldsMetaで指定したfieldsextra_kwargs'write_only': Trueとしていないものが取得入ってきます。
以下の例だとnameiddataで取得できるがpasswordは取得できなくなります。

user_serializer.py
class UserSerializer(ModelSerializer):

    class Meta:
        model = User
        fields = [
            'id',
            'name',
            'password'
        ]
        extra_kwargs = {'password': {'write_only': True}} # この場合to_repsentationの戻り値にpasswordは入っていこない

>>> user = User.objects.first()
>>> serialzier = UserSerializer(user, {'name': 'テスト', 'password': '1234password'})
>>> serialzier.data
error: You should either call `.is_valid()` first, or access `.initial_data` instead.
>>> serialzier.is_valid()
>>> serialzier.save()
>>> serialzier.data
{'id': 1, 'name' : 'テスト'}

まとめ

処理の順序と実装(オーバーライド)可能なメソッドを表にまとめてみました。

method 項目 役割 条件 オーバーライドすることでカスタマイズ可能なmehtod
is_valid データの整合性をチェックする - - validate
  - 引数: to_internal_valueの戻り値
  - 戻り値: validated_dataに詰められる、create, udpateの引数
- to_internal_value
  - 引数: 初期化時にdata引数
  - 戻り値: 内部で扱うように整形、validateメソッド等の引数
save データを保存する is_validが呼ばれており、エラーがない
初期化時にinstance引数を渡すとupdate
渡さないとcreateが呼ばれる
- create
  - 引数: validated_data
  - 戻り値: 新しく生成したインスタンス
- update:
  - 引数: validated_data
  - 戻り値: 新しく生成したインスタンス
data データの参照、表現 is_validが呼ばれている - to_representaion
  - 引数: createまたはupdateの戻り値
  - 戻り値: データ参照用のDict
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした