Repository: encode/django-rest-framework

Issue:
When validating input prevent `serializer.data` being accessed until `.is_valid()` is called.
See #2276, #2284 for examples of this causing confusion...

Folks attempting to access `.data` inside internal methods on serializer.

Error should probably mention `.initial_data`.



Discussion:

Resolving patch:
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -79,16 +79,14 @@ def get_serializer_context(self):
             'view': self
         }
 
-    def get_serializer(self, instance=None, data=None, many=False, partial=False):
+    def get_serializer(self, *args, **kwargs):
         """
         Return the serializer instance that should be used for validating and
         deserializing input, and for serializing output.
         """
         serializer_class = self.get_serializer_class()
-        context = self.get_serializer_context()
-        return serializer_class(
-            instance, data=data, many=many, partial=partial, context=context
-        )
+        kwargs['context'] = self.get_serializer_context()
+        return serializer_class(*args, **kwargs)
 
     def get_pagination_serializer(self, page):
         """
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -544,12 +544,12 @@ def get_rendered_html_form(self, data, view, method, request):
         # serializer instance, rather than dynamically creating a new one.
         if request.method == method and serializer is not None:
             try:
-                data = request.data
+                kwargs = {'data': request.data}
             except ParseError:
-                data = None
+                kwargs = {}
             existing_serializer = serializer
         else:
-            data = None
+            kwargs = {}
             existing_serializer = None
 
         with override_method(view, request, method) as request:
@@ -569,11 +569,13 @@ def get_rendered_html_form(self, data, view, method, request):
                 serializer = existing_serializer
             else:
                 if method in ('PUT', 'PATCH'):
-                    serializer = view.get_serializer(instance=instance, data=data)
+                    serializer = view.get_serializer(instance=instance, **kwargs)
                 else:
-                    serializer = view.get_serializer(data=data)
-                if data is not None:
-                    serializer.is_valid()
+                    serializer = view.get_serializer(**kwargs)
+
+            if hasattr(serializer, 'initial_data'):
+                serializer.is_valid()
+
             form_renderer = self.form_renderer_class()
             return form_renderer.render(
                 serializer.data,
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -58,11 +58,31 @@ class BaseSerializer(Field):
     """
     The BaseSerializer class provides a minimal class which may be used
     for writing custom serializer implementations.
+
+    Note that we strongly restrict the ordering of operations/properties
+    that may be used on the serializer in order to enforce correct usage.
+
+    In particular, if a `data=` argument is passed then:
+
+    .is_valid() - Available.
+    .initial_data - Available.
+    .validated_data - Only available after calling `is_valid()`
+    .errors - Only available after calling `is_valid()`
+    .data - Only available after calling `is_valid()`
+
+    If a `data=` argument is not passed then:
+
+    .is_valid() - Not available.
+    .initial_data - Not available.
+    .validated_data - Not available.
+    .errors - Not available.
+    .data - Available.
     """
 
-    def __init__(self, instance=None, data=None, **kwargs):
+    def __init__(self, instance=None, data=empty, **kwargs):
         self.instance = instance
-        self._initial_data = data
+        if data is not empty:
+            self.initial_data = data
         self.partial = kwargs.pop('partial', False)
         self._context = kwargs.pop('context', {})
         kwargs.pop('many', None)
@@ -156,9 +176,14 @@ def is_valid(self, raise_exception=False):
             (self.__class__.__module__, self.__class__.__name__)
         )
 
+        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)
+                self._validated_data = self.run_validation(self.initial_data)
             except ValidationError as exc:
                 self._validated_data = {}
                 self._errors = exc.detail
@@ -172,6 +197,16 @@ def is_valid(self, raise_exception=False):
 
     @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)
@@ -295,11 +330,11 @@ def get_validators(self):
         return getattr(getattr(self, 'Meta', None), 'validators', [])
 
     def get_initial(self):
-        if self._initial_data is not None:
+        if hasattr(self, 'initial_data'):
             return OrderedDict([
-                (field_name, field.get_value(self._initial_data))
+                (field_name, field.get_value(self.initial_data))
                 for field_name, field in self.fields.items()
-                if field.get_value(self._initial_data) is not empty
+                if field.get_value(self.initial_data) is not empty
                 and not field.read_only
             ])
 
@@ -447,8 +482,8 @@ def __init__(self, *args, **kwargs):
         self.child.bind(field_name='', parent=self)
 
     def get_initial(self):
-        if self._initial_data is not None:
-            return self.to_representation(self._initial_data)
+        if hasattr(self, 'initial_data'):
+            return self.to_representation(self.initial_data)
         return []
 
     def get_value(self, dictionary):


Test patch:
diff --git a/tests/test_bound_fields.py b/tests/test_bound_fields.py
--- a/tests/test_bound_fields.py
+++ b/tests/test_bound_fields.py
@@ -22,7 +22,7 @@ class ExampleSerializer(serializers.Serializer):
             amount = serializers.IntegerField()
 
         serializer = ExampleSerializer(data={'text': 'abc', 'amount': 123})
-
+        assert serializer.is_valid()
         assert serializer['text'].value == 'abc'
         assert serializer['text'].errors is None
         assert serializer['text'].name == 'text'