Skip to content

Commit 76cfc5e

Browse files
committed
Merge pull request #2291 from tomchristie/serializer-api-restrictions
Serializer API restrictions.
2 parents 426547c + c6137bb commit 76cfc5e

File tree

5 files changed

+62
-21
lines changed

5 files changed

+62
-21
lines changed

docs/api-guide/serializers.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,12 @@ Serializer classes can also include reusable validators that are applied to the
240240

241241
For more information see the [validators documentation](validators.md).
242242

243+
## Accessing the initial data and instance
244+
245+
When passing an initial object or queryset to a serializer instance, the object will be made available as `.instance`. If no initial object is passed then the `.instance` attribute will be `None`.
246+
247+
When passing data to a serializer instance, the unmodified data will be made available as `.initial_data`. If the data keyword argument is not passed then the `.initial_data` attribute will not exist.
248+
243249
## Partial updates
244250

245251
By default, serializers must be passed values for all required fields or they will raise validation errors. You can use the `partial` argument in order to allow partial updates.

rest_framework/generics.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,14 @@ def get_serializer_context(self):
7979
'view': self
8080
}
8181

82-
def get_serializer(self, instance=None, data=None, many=False, partial=False):
82+
def get_serializer(self, *args, **kwargs):
8383
"""
8484
Return the serializer instance that should be used for validating and
8585
deserializing input, and for serializing output.
8686
"""
8787
serializer_class = self.get_serializer_class()
88-
context = self.get_serializer_context()
89-
return serializer_class(
90-
instance, data=data, many=many, partial=partial, context=context
91-
)
88+
kwargs['context'] = self.get_serializer_context()
89+
return serializer_class(*args, **kwargs)
9290

9391
def get_pagination_serializer(self, page):
9492
"""

rest_framework/renderers.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -544,12 +544,12 @@ def get_rendered_html_form(self, data, view, method, request):
544544
# serializer instance, rather than dynamically creating a new one.
545545
if request.method == method and serializer is not None:
546546
try:
547-
data = request.data
547+
kwargs = {'data': request.data}
548548
except ParseError:
549-
data = None
549+
kwargs = {}
550550
existing_serializer = serializer
551551
else:
552-
data = None
552+
kwargs = {}
553553
existing_serializer = None
554554

555555
with override_method(view, request, method) as request:
@@ -569,11 +569,13 @@ def get_rendered_html_form(self, data, view, method, request):
569569
serializer = existing_serializer
570570
else:
571571
if method in ('PUT', 'PATCH'):
572-
serializer = view.get_serializer(instance=instance, data=data)
572+
serializer = view.get_serializer(instance=instance, **kwargs)
573573
else:
574-
serializer = view.get_serializer(data=data)
575-
if data is not None:
576-
serializer.is_valid()
574+
serializer = view.get_serializer(**kwargs)
575+
576+
if hasattr(serializer, 'initial_data'):
577+
serializer.is_valid()
578+
577579
form_renderer = self.form_renderer_class()
578580
return form_renderer.render(
579581
serializer.data,

rest_framework/serializers.py

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,31 @@ class BaseSerializer(Field):
5858
"""
5959
The BaseSerializer class provides a minimal class which may be used
6060
for writing custom serializer implementations.
61+
62+
Note that we strongly restrict the ordering of operations/properties
63+
that may be used on the serializer in order to enforce correct usage.
64+
65+
In particular, if a `data=` argument is passed then:
66+
67+
.is_valid() - Available.
68+
.initial_data - Available.
69+
.validated_data - Only available after calling `is_valid()`
70+
.errors - Only available after calling `is_valid()`
71+
.data - Only available after calling `is_valid()`
72+
73+
If a `data=` argument is not passed then:
74+
75+
.is_valid() - Not available.
76+
.initial_data - Not available.
77+
.validated_data - Not available.
78+
.errors - Not available.
79+
.data - Available.
6180
"""
6281

63-
def __init__(self, instance=None, data=None, **kwargs):
82+
def __init__(self, instance=None, data=empty, **kwargs):
6483
self.instance = instance
65-
self._initial_data = data
84+
if data is not empty:
85+
self.initial_data = data
6686
self.partial = kwargs.pop('partial', False)
6787
self._context = kwargs.pop('context', {})
6888
kwargs.pop('many', None)
@@ -156,9 +176,14 @@ def is_valid(self, raise_exception=False):
156176
(self.__class__.__module__, self.__class__.__name__)
157177
)
158178

179+
assert hasattr(self, 'initial_data'), (
180+
'Cannot call `.is_valid()` as no `data=` keyword argument was'
181+
'passed when instantiating the serializer instance.'
182+
)
183+
159184
if not hasattr(self, '_validated_data'):
160185
try:
161-
self._validated_data = self.run_validation(self._initial_data)
186+
self._validated_data = self.run_validation(self.initial_data)
162187
except ValidationError as exc:
163188
self._validated_data = {}
164189
self._errors = exc.detail
@@ -172,6 +197,16 @@ def is_valid(self, raise_exception=False):
172197

173198
@property
174199
def data(self):
200+
if hasattr(self, 'initial_data') and not hasattr(self, '_validated_data'):
201+
msg = (
202+
'When a serializer is passed a `data` keyword argument you '
203+
'must call `.is_valid()` before attempting to access the '
204+
'serialized `.data` representation.\n'
205+
'You should either call `.is_valid()` first, '
206+
'or access `.initial_data` instead.'
207+
)
208+
raise AssertionError(msg)
209+
175210
if not hasattr(self, '_data'):
176211
if self.instance is not None and not getattr(self, '_errors', None):
177212
self._data = self.to_representation(self.instance)
@@ -295,11 +330,11 @@ def get_validators(self):
295330
return getattr(getattr(self, 'Meta', None), 'validators', [])
296331

297332
def get_initial(self):
298-
if self._initial_data is not None:
333+
if hasattr(self, 'initial_data'):
299334
return OrderedDict([
300-
(field_name, field.get_value(self._initial_data))
335+
(field_name, field.get_value(self.initial_data))
301336
for field_name, field in self.fields.items()
302-
if field.get_value(self._initial_data) is not empty
337+
if field.get_value(self.initial_data) is not empty
303338
and not field.read_only
304339
])
305340

@@ -447,8 +482,8 @@ def __init__(self, *args, **kwargs):
447482
self.child.bind(field_name='', parent=self)
448483

449484
def get_initial(self):
450-
if self._initial_data is not None:
451-
return self.to_representation(self._initial_data)
485+
if hasattr(self, 'initial_data'):
486+
return self.to_representation(self.initial_data)
452487
return []
453488

454489
def get_value(self, dictionary):

tests/test_bound_fields.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class ExampleSerializer(serializers.Serializer):
2222
amount = serializers.IntegerField()
2323

2424
serializer = ExampleSerializer(data={'text': 'abc', 'amount': 123})
25-
25+
assert serializer.is_valid()
2626
assert serializer['text'].value == 'abc'
2727
assert serializer['text'].errors is None
2828
assert serializer['text'].name == 'text'

0 commit comments

Comments
 (0)