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 |