Repository: aio-libs/aiohttp

Issue:
Add json_response funciton
Should be derived from `aiohttp.web.Response`.

Constructor signature is:
`def __init__(self, data, *, status=200, reason=None, headers=None)`

Should pack `data` arg as `json.dumps()` and set content type to `application/json`.

People forget to specify proper content type on sending json data.



Discussion:
Any preference as to if/how the `json` module should be specified?
- Fallback

``` python
try:
    from simplejson as json  # or anyjson? ujson?
except ImportError:
    import json
```
- Class variable

``` python
import json

class JSONResponse(Response):
   json_module = json
    #...
```

I think constructor parameter `dumps=json.dumps` (like https://github.com/KeepSafe/aiohttp/blob/master/aiohttp/client_reqrep.py#L728) should be enough for the issue.

Another pull request may try to solve global configuration problem. See discussion for #336 also.

 Any reason not to pass in the `json_module=json`, just in case we ever need to deserialize JSON.

Hypothetical use case:

``` python
class JSONResponse(Response):

    @classmethod
    def from_json(cls, json_string: str, **kwargs):
        return cls(data=self.json_module.loads(json_string), **kwargs)
```

I don't see use case for deserialization of response. 
`web.Request` has `.json()` coroutine for deserializing request https://github.com/KeepSafe/aiohttp/blob/master/aiohttp/web_reqrep.py#L326

Different libraries may have different functions for unpacking data but I pretty sure all of them provides callable accepting dict and returning string.
Also I may want to use custom encoder/decoder: https://docs.python.org/3/library/json.html#encoders-and-decoders

Hi,

If you could do something like that: https://github.com/Eyepea/API-Hour/blob/master/api_hour/plugins/aiohttp/__init__.py

It means we'll can remove this file: less code in API-Hour, better is ;-)

We use a lot this helper.

In this file, we have also an helper to output HTML  files, be my guest to do something similar. 

I'm not reluctant for adding `HTMLResponse` too.
Perhaps we need another issue for that.
@GMLudo would you file this one?

@asvetlov 
Why not just leave JSON serialization to the user, like in `HTMLResponse`?
Like you said, the problem is that people forget to set `content-type: application/json`, not serialization.
IMHO, a simple class that forces the content-type to `application/json` should suffice.

@jashandeep-sohi I believe `JsonResponse(json.dumps(data))` looks uglier than `JsonResponse(data)`. 

Advanced user may provide serializer is needed.

> Should pack data arg as json.dumps() and set content type to application/json.

I only note, that content-type should be easily customized and not just as constant value, but with the respect of Accept header. Cases:
1. When we develop API and use own JSON MIME type like `application/vnd.my-company+json`;
2. When we need to return `text/plain` for browsers in order to let us safely open API url in it and see the response instead of being asked to download some cryptic file. However, for explicit Accept `application/json` we must return expected `application/json` Content-Type. Tradeoffs for usability are hard.

These details makes life a bit complicated, but not too much.

> I believe JsonResponse(json.dumps(data)) looks uglier than JsonResponse(data). 

Agree. It's also quite awkward that JsonResponse doesn't know how to transform data into JSON.

@kxepal 
Well, neither will `HTMLResponse`, if that's ever added.

@asvetlov 
Depends on you perspective, I suppose. I wouldn't say its uglier, but more verbose.

So, how would one go about returning a cached JSON string using this scheme?

@jashandeep-sohi basically, all these *Response is about to get some data, pass it through render function and form HTTPResponse with proper related metadata (status, headers, payload etc.). 

For JsonResponse this renderer is json.dump or whatever, for HTML one is jinja.render or special bridge to php or whatever. So both objects shares similar protocol and workflow. What you only need is to be able customize their behavior and be sure to not violate it.

Okay, so `HTMLResponse` would have a similar `dumps` argument to the constructor?

@jashandeep-sohi I take you proposal for adding `dumps` arg to `HTMLResponse` and a joke to stress your opinion.

But yes, we can relax parameter list a bit to make both `content_type` and `data` optional. User may override content type if needed and pass non `data` positional arg but `text` or even `body` named args.

Anyway, I feel we need Pull Request for further discussion.

Volunteers?

> I take you proposal for adding dumps arg to HTMLResponse and a joke to stress your opinion.

Sorry, something must be getting lost in translation/typing, but I wasn't joking. I was genuinely asking how the `HTMLResponse` class would look like.

@jashandeep-sohi 
Sorry. My English is a far from fluent.

What `dumps` parameter for `HTMPResponse` should do?
Rendering by `jinja2` or `mako` requires a lot of work for setting up renderer (thus we've created `aiohttp_jinja2` and `aiohttp_mako` libraries).
But for json you can use just `dumps` function from json, simplejson or ujson in most cases.
For complex usage scenarios you may create custom callable. 

Okay, understood.


Resolving patch:
diff --git a/aiohttp/web_reqrep.py b/aiohttp/web_reqrep.py
--- a/aiohttp/web_reqrep.py
+++ b/aiohttp/web_reqrep.py
@@ -26,7 +26,9 @@
 from .streams import EOF_MARKER
 
 
-__all__ = ('ContentCoding', 'Request', 'StreamResponse', 'Response')
+__all__ = (
+    'ContentCoding', 'Request', 'StreamResponse', 'Response', 'JSONResponse'
+)
 
 
 sentinel = object()
@@ -810,3 +812,18 @@ def write_eof(self):
         if body is not None:
             self.write(body)
         yield from super().write_eof()
+
+
+class JSONResponse(Response):
+
+    def __init__(self, data=sentinel, *, text=None, body=None, status=200,
+                 reason=None, headers=None, content_type='application/json',
+                 dumps=json.dumps):
+        if data is not sentinel and (text or body):
+            raise ValueError(
+                'only one of data, text, or body should be specified'
+            )
+        text = text or dumps(data)
+        content_type = content_type or self.content_type
+        super().__init__(text=text, body=body, status=status, reason=reason,
+                         content_type=content_type)


Test patch:
diff --git a/tests/test_web_response.py b/tests/test_web_response.py
--- a/tests/test_web_response.py
+++ b/tests/test_web_response.py
@@ -1,10 +1,13 @@
 import datetime
+import json
 import pytest
 import re
 from unittest import mock
 from aiohttp import hdrs, signals
 from aiohttp.multidict import CIMultiDict
-from aiohttp.web import ContentCoding, Request, StreamResponse, Response
+from aiohttp.web import (
+    ContentCoding, Request, StreamResponse, Response, JSONResponse
+)
 from aiohttp.protocol import HttpVersion, HttpVersion11, HttpVersion10
 from aiohttp.protocol import RawRequestMessage
 
@@ -836,3 +839,46 @@ def test_text_with_empty_payload():
     resp = Response(status=200)
     assert resp.body is None
     assert resp.text is None
+
+
+class TestJSONResponse:
+
+    def test_content_type_is_application_json_by_default(self):
+        resp = JSONResponse('')
+        assert 'application/json' == resp.content_type
+
+    def test_passing_text_only(self):
+        resp = JSONResponse(text=json.dumps('jaysawn'))
+        assert resp.text == json.dumps('jaysawn')
+
+    def test_data_and_text_raises_value_error(self):
+        with pytest.raises(ValueError) as excinfo:
+            JSONResponse(data='foo', text='bar')
+        expected_message = (
+            'only one of data, text, or body should be specified'
+        )
+        assert expected_message == excinfo.value.args[0]
+
+    def test_data_and_body_raises_value_error(self):
+        with pytest.raises(ValueError) as excinfo:
+            JSONResponse(data='foo', body=b'bar')
+        expected_message = (
+            'only one of data, text, or body should be specified'
+        )
+        assert expected_message == excinfo.value.args[0]
+
+    def test_text_is_json_encoded(self):
+        resp = JSONResponse({'foo': 42})
+        assert json.dumps({'foo': 42}) == resp.text
+
+    def test_content_type_is_overrideable(self):
+        resp = JSONResponse({'foo': 42},
+                            content_type='application/vnd.json+api')
+        assert 'application/vnd.json+api' == resp.content_type
+
+    def test_content_type_is_overrideable_as_class_var(self):
+        class MyJSONResponse(JSONResponse):
+            content_type = 'application/vnd.json+api'
+
+        resp = MyJSONResponse('jaysawhn')
+        assert 'application/vnd.json+api' == resp.content_type