DjangoのSerializerでいつ何がどう呼ばれてどのmethodをどう実装すればよいかがわからないことがあります。
公式ドキュメントを読んでもいまいちどう実装すればいいかわかないという人(自分も具体的な使い方が想像できませんでした。)も多いと思うのでよく使う以下の3つのメソッド/プロパティについて中のModelSerializerクラスのソースを実際に読みながらまとめてみました。
- save
- is_valid
- data
serializer.save()
まずはObject作成時や更新時に呼ぶメソッドの解説をします。
一部省略していますが、saveは以下のように実装されています。
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プロパティを参照することはできません。
②の解説
引数で渡したkwargs
とvalidated_data
を結合して新たなvalidated_data
を生成しています。
kwargs
は引数で渡したものそのものなので、そのままです。
ではvalidated_data
はどこで生成されてるかというと、これもis_valid
で生成される_validated_data
を参照するプロパティです。
@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があるかないかでupdate
かcreate
を呼ぶかが切り替わります。
instanceが生成されるかどうかは初期化時に引数を渡すかどうかで決まります。
以下が__init__
の中身です。
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
クラスではこのupdate
やcreate
はabstractmethodでオーバーライドせずに呼び出した場合、NotImplementedError
となりますが
ModelSerializer
クラスではMeta
クラスよりデフォルトでupdateやcreateが実装されています。
serializer.is_valid()
続いて先程のsave
でも多く登場したis_valid
を解説します。
ソースは以下のようになっています。
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
で何をしているかを見てみます。
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
はデフォルトでは引数をそのままリターンするだけのメソッドです。
def validate(self, attrs):
return attrs
validate
をオーバーライドすることで独自のvalidationとvalidate_dataの生成が可能になります。
serializer.data
dataの中身は以下のようになっています。
@property
def data(self):
ret = super(Serializer, self).data
return ReturnDict(ret, serializer=self)
スーパークラスのSerializer
クラスを参照しているのでそちらを見ると
@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_data
はModelSerializer
では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
を見てみましょう。
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_fields
はMeta
で指定したfields
でextra_kwargs
に'write_only': True
としていないものが取得入ってきます。
以下の例だとname
、id
はdata
で取得できるがpassword
は取得できなくなります。
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 |