Repository: encode/django-rest-framework

Issue:
Guard against calling `serializer.data` before `serializer.save()`
It appears that accessing `serializer.data` has side effects. Merely accessing the property inside `perform_create()` will cause the response data to change. I created a simple Django project with a test case that illustrates the bug: [django-rest-bug](https://github.com/kylefox/django-rest-bug).

The expected behavior is for `response.data` to contain the serialized version of the created object:

``` python
class AlbumViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):

    # ... snip ...

    def perform_create(self, serializer):
        serializer.save()

# >>> post_data = {'title': 'The Wall', 'artist': 'Pink Floyd'}
# >>> response = self.client.post(reverse('album-list'), post_data)
# >>> print response.data
# {'id': 1, 'artist': u'Pink Floyd', 'title': u'The Wall'}
```

But when `serializer.data` is present it will cause `response.data` to be equal to the initially posted data (ex: the `id` is missing from response):

``` python
class AlbumViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):

    # ... snip ...

    def perform_create(self, serializer):
        serializer.data  # This line being present changes the value of `response.data`
        serializer.save()

# >>> post_data = {'title': 'The Wall', 'artist': 'Pink Floyd'}
# >>> response = self.client.post(reverse('album-list'), post_data)
# >>> print response.data
# {'artist': u'Pink Floyd', 'title': u'The Wall'}
```

Even if `serializer.data` is not "safe", I think it's incorrect for property access to result in side effects. I _think_ the culprit is somewhere in [`BaseSerializer.data`](https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/serializers.py#L210) but am not certain.



Discussion:
I'd be interested in hearing why you want to access the `serializer.data` within the view.

The flow you've demonstrated there isn't intended usage, and it's possible that its something we should guard against. (Ie raise an exception if `.save()` is called _after_ accessing `.data`)

It's valid to access either `.initial_data` or `.validated_data` prior to `.save()`, but it shouldn't be valid to access `.data` and then subsequently call `.save()`.

What's happening internally is that `serializer.data` is serializing the _unsaved_ instance, because you've called it prior to save.

When we properly restrict the flow then effectively our properties don't have side effects, but this is a case we've obviously left unguarded. 

Retitled this issue to reflect the change we should make.

@xordoquy I was attempting to use it for some validation/permission logic before creating the object. Let's say a `User` is only allowed to create `Messages` attached to a `Project` they have access to. In that case `MessageViewSet. perform_create` would look like this:

``` python
class MessageViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):

    def perform_create(self, serializer):
        project = serializer.data['project']
        if project in self.request.user.projects.all():
            serializer.save(project=project)
        else:
            raise exceptions.PermissionDenied
```

It feels wrong to put this into the view, but I wasn't sure where else to put it. I guess this might be a case for creating a validation method in my serializer which uses `.context` to access `request.user` and validate there?

@tomchristie Good to know, thanks. I actually found, through experimentation, that `.validated_data` worked properly in this scenario. Glad to know this is the correct way to access it :) Though as I mentioned above I think now I understand the correct way to implement what I'm trying to do (`.context`). I suppose it's not _as critical_ to prevent side effects when this flow/scenario isn't intended usage :sweat_smile:

Not critical no, but preferable, so let's keep this open for now.

> I guess this might be a case for creating a validation method in my serializer which uses .context to access request.user and validate there?

Yup. Validation should really be kept inside of the serializer. Or at least it should happen before `perform_create` is called.

I usually recommend people limit the `[serializer_field].queryset` property during the initialization of the serializer so the user gets an error if they try to set the relation to something invalid.

@kevin-brown Thanks for pointing me in that direction — this is what I wound up with in my serializer:

``` python
class MessageSerializer(serializers.ModelSerializer):

    class Meta:
        model = Message
        fields = ('id', 'project', 'body')

    def __init__(self, *args, **kwargs):
        super(MessageSerializer, self).__init__(*args, **kwargs)
        self.user = self.context['request'].user
        self.fields['project'].queryset = self.user.projects.all()

    def validate_project(self, project):
        if project not in self.user.projects.all():
            raise exceptions.PermissionDenied
        return project
```

Because I'm already limiting the queryset in `__init__`, maybe I can remove `validate_project()` entirely...


Resolving patch:
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -166,6 +166,12 @@ def save(self, **kwargs):
             "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())


Test patch:
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
--- a/tests/test_serializer.py
+++ b/tests/test_serializer.py
@@ -51,6 +51,16 @@ class MissingAttributes:
         with pytest.raises(AttributeError):
             serializer.data
 
+    def test_data_access_before_save_raises_error(self):
+        def create(validated_data):
+            return validated_data
+        serializer = self.Serializer(data={'char': 'abc', 'integer': 123})
+        serializer.create = create
+        assert serializer.is_valid()
+        assert serializer.data == {'char': 'abc', 'integer': 123}
+        with pytest.raises(AssertionError):
+            serializer.save()
+
 
 class TestValidateMethod:
     def test_non_field_error_validate_method(self):