Repository: encode/django-rest-framework

Issue:
Empty requests treated differently from '{}'
This took me quite a while to work out :)

I have a Serializer with HyperlinkedRelatedField(many=True) on it. I had some API tests that would pass in an empty dict:

```
self.client.post(list_url, {}, format='json')
```

The response yielded the expected 401, stating that the given field was required. So far, so good.

Much to my surprise, if I sent an empty POST request to the same url on my production system (or Django's built-in development server), the resource was created succesfully, with an empty list for that field.

After quite a bit of digging, I learned that an empty request invariably means that request.data is a QueryDict, rather than a simple, empty dict. During validation, many Field implementations call html.is_html_dictionary() on request.data. is_html_dictionary simply checks for a .getlist() method. Incidentally, this getlist() method is the culprit. When called, it will always return a list, even if the given key is not found at all, in which case it yields an empty list.

I feel that the result for an empty request and a request that passes in an empty json object should yield the exact same results.



Discussion:
Actually HTML forms and JSON requests differs a lot because there's no way we can  have as much details with the forms as we have with JSON.
Will keep this open, need to think if we can improve things here.

I appreciate that, but I'm not using HTML forms anywhere. I'm even explicitly passing "application/json" as my content type.

It's not very elegant, but this fixes it for me and doesn't break any tests:

```
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index 2dc808f..d99fef9 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -62,6 +62,9 @@ class JSONParser(BaseParser):
         parser_context = parser_context or {}
         encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)

+        if not stream:
+            return {}
+
         try:
             data = stream.read().decode(encoding)
             return json.loads(data)
diff --git a/rest_framework/request.py b/rest_framework/request.py
index 846cadc..785abd7 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -275,7 +275,7 @@ class Request(object):
         stream = self.stream
         media_type = self.content_type

-        if stream is None or media_type is None:
+        if stream is None and not media_type:
             empty_data = QueryDict('', encoding=self._request._encoding)
             empty_files = MultiValueDict()
             return (empty_data, empty_files)
```

@sorenh I think a failing test case would help more. Your workaround may not work with nested serializers for example.

I'm working on a good test case right now. I'll have it ready tomorrow, probably.

Thanks, this will help a lot !

I don't know.. I have test that fails, but I'm not sure if it's too specific:

https://github.com/sorenh/django-rest-framework/commit/d9a41859208e5dfa3860704b764f083e398182e0

Hi,

I have a problem that could maybe similar (if not, I can create new issue).
When a send a empty `PUT`, `json` or `form-url-encoded`, my `request.data` (`QueryDict`) is immutable.
If I just add a single attribut in my `json`, my `request.data` (`QueryDict`) is mutable.

Any idea? Is it related?

Thank you guys! :smiley: 

Edit: `djangorestframework==3.2.4`


Resolving patch:
diff --git a/rest_framework/request.py b/rest_framework/request.py
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -299,7 +299,10 @@ def _parse(self):
             stream = None
 
         if stream is None or media_type is None:
-            empty_data = QueryDict('', encoding=self._request._encoding)
+            if media_type and not is_form_media_type(media_type):
+                empty_data = QueryDict('', encoding=self._request._encoding)
+            else:
+                empty_data = {}
             empty_files = MultiValueDict()
             return (empty_data, empty_files)
 


Test patch:
diff --git a/tests/test_testing.py b/tests/test_testing.py
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -8,6 +8,7 @@
 from django.shortcuts import redirect
 from django.test import TestCase, override_settings
 
+from rest_framework import fields, serializers
 from rest_framework.decorators import api_view
 from rest_framework.response import Response
 from rest_framework.test import (
@@ -37,10 +38,22 @@ def redirect_view(request):
     return redirect('/view/')
 
 
+class BasicSerializer(serializers.Serializer):
+    flag = fields.BooleanField(default=lambda: True)
+
+
+@api_view(['POST'])
+def post_view(request):
+    serializer = BasicSerializer(data=request.data)
+    serializer.is_valid(raise_exception=True)
+    return Response(serializer.validated_data)
+
+
 urlpatterns = [
     url(r'^view/$', view),
     url(r'^session-view/$', session_view),
     url(r'^redirect-view/$', redirect_view),
+    url(r'^post-view/$', post_view)
 ]
 
 
@@ -181,6 +194,15 @@ def test_invalid_multipart_data(self):
             path='/view/', data={'valid': 123, 'invalid': {'a': 123}}
         )
 
+    def test_empty_post_uses_default_boolean_value(self):
+        response = self.client.post(
+            '/post-view/',
+            data=None,
+            content_type='application/json'
+        )
+        self.assertEqual(response.status_code, 200, response.content)
+        self.assertEqual(response.data, {"flag": True})
+
 
 class TestAPIRequestFactory(TestCase):
     def test_csrf_exempt_by_default(self):