Fixed #17942 -- Added a JsonResponse class to more easily create JSON encoded responses.

Thanks leahculver for the suggestion and Erik Romijn,
Simon Charette, and Marc Tamlyn for the reviews.
This commit is contained in:
Lukasz Balcerzak 2014-02-14 18:28:51 +01:00 committed by Tim Graham
parent e3d0790bd0
commit 0242134d32
9 changed files with 176 additions and 8 deletions

View File

@ -5,7 +5,7 @@ from django.http.response import (HttpResponse, StreamingHttpResponse,
HttpResponseRedirect, HttpResponsePermanentRedirect,
HttpResponseNotModified, HttpResponseBadRequest, HttpResponseForbidden,
HttpResponseNotFound, HttpResponseNotAllowed, HttpResponseGone,
HttpResponseServerError, Http404, BadHeaderError)
HttpResponseServerError, Http404, BadHeaderError, JsonResponse)
from django.http.utils import (fix_location_header,
conditional_content_removal, fix_IE_for_attach, fix_IE_for_vary)
@ -16,6 +16,6 @@ __all__ = [
'HttpResponsePermanentRedirect', 'HttpResponseNotModified',
'HttpResponseBadRequest', 'HttpResponseForbidden', 'HttpResponseNotFound',
'HttpResponseNotAllowed', 'HttpResponseGone', 'HttpResponseServerError',
'Http404', 'BadHeaderError', 'fix_location_header',
'Http404', 'BadHeaderError', 'fix_location_header', 'JsonResponse',
'conditional_content_removal', 'fix_IE_for_attach', 'fix_IE_for_vary',
]

View File

@ -1,8 +1,9 @@
from __future__ import unicode_literals
import datetime
import time
import json
import sys
import time
from email.header import Header
try:
from urllib.parse import urlparse
@ -13,6 +14,7 @@ from django.conf import settings
from django.core import signals
from django.core import signing
from django.core.exceptions import DisallowedRedirect
from django.core.serializers.json import DjangoJSONEncoder
from django.http.cookie import SimpleCookie
from django.utils import six, timezone
from django.utils.encoding import force_bytes, force_text, iri_to_uri
@ -456,3 +458,25 @@ class HttpResponseServerError(HttpResponse):
class Http404(Exception):
pass
class JsonResponse(HttpResponse):
"""
An HTTP response class that consumes data to be serialized to JSON.
:param data: Data to be dumped into json. By default only ``dict`` objects
are allowed to be passed due to a security flaw before EcmaScript 5. See
the ``safe`` parameter for more information.
:param encoder: Should be an json encoder class. Defaults to
``django.core.serializers.json.DjangoJSONEncoder``.
:param safe: Controls if only ``dict`` objects may be serialized. Defaults
to ``True``.
"""
def __init__(self, data, encoder=DjangoJSONEncoder, safe=True, **kwargs):
if safe and not isinstance(data, dict):
raise TypeError('In order to allow non-dict objects to be '
'serialized set the safe parameter to False')
kwargs.setdefault('content_type', 'application/json')
data = json.dumps(data, cls=encoder)
super(JsonResponse, self).__init__(content=data, **kwargs)

View File

@ -36,7 +36,7 @@ All attributes should be considered read-only, unless stated otherwise below.
.. versionadded:: 1.7
A string representing the scheme of the request (``http`` or ``https``
A string representing the scheme of the request (``http`` or ``https``
usually).
.. attribute:: HttpRequest.body
@ -823,6 +823,74 @@ types of HTTP responses. Like ``HttpResponse``, these subclasses live in
:class:`~django.template.response.SimpleTemplateResponse`, and the
``render`` method must itself return a valid response object.
JsonResponse objects
====================
.. versionadded:: 1.7
.. class:: JsonResponse
.. method:: JsonResponse.__init__(data, encoder=DjangoJSONEncoder, safe=True, **kwargs)
An :class:`HttpResponse` subclass that helps to create a JSON-encoded
response. It inherits most behavior from its superclass with a couple
differences:
Its default ``Content-Type`` header is set to ``application/json``.
The first parameter, ``data``, should be a ``dict`` instance. If the ``safe``
parameter is set to ``False`` (see below) it can be any JSON-serializable
object.
The ``encoder``, which defaults to
``django.core.serializers.json.DjangoJSONEncoder``, will be used to
serialize the data. See :ref:`JSON serialization
<serialization-formats-json>` for more details about this serializer.
The ``safe`` boolean parameter defaults to ``True``. If it's set to ``False``,
any object can be passed for serialization (otherwise only ``dict`` instances
are allowed). If ``safe`` is ``True`` and a non-``dict`` object is passed as
the first argument, a :exc:`~exceptions.TypeError` will be raised.
Usage
-----
Typical usage could look like::
>>> from django.http import JsonResponse
>>> response = JsonResponse({'foo': 'bar'})
>>> response.content
'{"foo": "bar"}'
Serializing non-dictionary objects
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In order to serialize objects other than ``dict`` you must set the ``safe``
parameter to ``False``::
>>> response = JsonResponse([1, 2, 3], safe=False)
Without passing ``safe=False``, a :exc:`~exceptions.TypeError` will be raised.
.. warning::
Before the `5th edition of EcmaScript
<http://www.ecma-international.org/publications/standards/Ecma-262.htm>`_
it was possible to poison the JavaScript ``Array`` constructor. For this
reason, Django does not allow passing non-dict objects to the
:class:`~django.http.JsonResponse` constructor by default. However, most
modern browsers implement EcmaScript 5 which removes this attack vector.
Therefore it is possible to disable this security precaution.
Changing the default JSON encoder
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you need to use a differ JSON encoder class you can pass the ``encoder``
parameter to the constructor method::
>>> response = JsonResponse(data, encoder=MyJSONEncoder)
.. _httpresponse-streaming:
StreamingHttpResponse objects

View File

@ -671,15 +671,19 @@ Templates
* The new :tfilter:`truncatechars_html` filter truncates a string to be no
longer than the specified number of characters, taking HTML into account.
Requests
^^^^^^^^
Requests and Responses
^^^^^^^^^^^^^^^^^^^^^^
* The new :attr:`HttpRequest.scheme <django.http.HttpRequest.scheme>` attribute
specifies the scheme of the request (``http`` or ``https`` normally).
* The shortcut :func:`redirect() <django.shortcuts.redirect>` now supports
relative URLs.
* The new :class:`~django.http.JsonResponse` subclass of
:class:`~django.http.HttpResponse` helps easily create JSON-encoded responses.
Tests
^^^^^

View File

@ -216,6 +216,8 @@ the auth.User model has such a relation to the auth.Permission model::
This example links the given user with the permission models with PKs 46 and 47.
.. _serialization-formats-json:
JSON
~~~~

View File

@ -2,18 +2,20 @@
from __future__ import unicode_literals
import copy
import json
import os
import pickle
import unittest
import warnings
from django.core.exceptions import SuspiciousOperation
from django.core.serializers.json import DjangoJSONEncoder
from django.core.signals import request_finished
from django.db import close_old_connections
from django.http import (QueryDict, HttpResponse, HttpResponseRedirect,
HttpResponsePermanentRedirect, HttpResponseNotAllowed,
HttpResponseNotModified, StreamingHttpResponse,
SimpleCookie, BadHeaderError,
SimpleCookie, BadHeaderError, JsonResponse,
parse_cookie)
from django.test import TestCase
from django.utils.encoding import smart_str, force_text
@ -451,6 +453,35 @@ class HttpResponseSubclassesTests(TestCase):
self.assertContains(response, 'Only the GET method is allowed', status_code=405)
class JsonResponseTests(TestCase):
def test_json_response_non_ascii(self):
data = {'key': 'łóżko'}
response = JsonResponse(data)
self.assertEqual(json.loads(response.content.decode()), data)
def test_json_response_raises_type_error_with_default_setting(self):
with self.assertRaisesMessage(TypeError,
'In order to allow non-dict objects to be serialized set the '
'safe parameter to False'):
JsonResponse([1, 2, 3])
def test_json_response_text(self):
response = JsonResponse('foobar', safe=False)
self.assertEqual(json.loads(response.content.decode()), 'foobar')
def test_json_response_list(self):
response = JsonResponse(['foo', 'bar'], safe=False)
self.assertEqual(json.loads(response.content.decode()), ['foo', 'bar'])
def test_json_response_custom_encoder(self):
class CustomDjangoJSONEncoder(DjangoJSONEncoder):
def encode(self, o):
return json.dumps({'foo': 'bar'})
response = JsonResponse({}, encoder=CustomDjangoJSONEncoder)
self.assertEqual(json.loads(response.content.decode()), {'foo': 'bar'})
class StreamingHttpResponseTests(TestCase):
def test_streaming_response(self):
r = StreamingHttpResponse(iter(['hello', 'world']))

View File

@ -56,3 +56,8 @@ urlpatterns += patterns('view_tests.views',
(r'^shortcuts/render/dirs/$', 'render_with_dirs'),
(r'^shortcuts/render/current_app_conflict/$', 'render_view_with_current_app_conflict'),
)
# json response
urlpatterns += patterns('view_tests.views',
(r'^json/response/$', 'json_response_view'),
)

View File

@ -0,0 +1,22 @@
# encoding: utf8
from __future__ import unicode_literals
import json
from django.test import TestCase
class JsonResponseTests(TestCase):
urls = 'view_tests.generic_urls'
def test_json_response(self):
response = self.client.get('/json/response/')
self.assertEqual(response.status_code, 200)
self.assertEqual(
response['content-type'], 'application/json')
self.assertEqual(json.loads(response.content.decode()), {
'a': [1, 2, 3],
'foo': {'bar': 'baz'},
'timestamp': '2013-05-19T20:00:00',
'value': '3.14',
})

View File

@ -1,11 +1,13 @@
from __future__ import unicode_literals
import datetime
import decimal
import os
import sys
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.urlresolvers import get_resolver
from django.http import HttpResponse, HttpResponseRedirect
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import render_to_response, render
from django.template import Context, RequestContext, TemplateDoesNotExist
from django.views.debug import technical_500_response, SafeExceptionReporterFilter
@ -334,3 +336,13 @@ def multivalue_dict_key_error(request):
exc_info = sys.exc_info()
send_log(request, exc_info)
return technical_500_response(request, *exc_info)
def json_response_view(request):
return JsonResponse({
'a': [1, 2, 3],
'foo': {'bar': 'baz'},
# Make sure datetime and Decimal objects would be serialized properly
'timestamp': datetime.datetime(2013, 5, 19, 20),
'value': decimal.Decimal('3.14'),
})