Skip to content

Instantly share code, notes, and snippets.

@mminer
Created April 26, 2013 02:36
Show Gist options
  • Save mminer/5464753 to your computer and use it in GitHub Desktop.
Save mminer/5464753 to your computer and use it in GitHub Desktop.
A JSON request handler for Tornado.
import json
import tornado.web
class JsonHandler(BaseHandler):
"""Request handler where requests and responses speak JSON."""
def prepare(self):
# Incorporate request JSON into arguments dictionary.
if self.request.body:
try:
json_data = json.loads(self.request.body)
self.request.arguments.update(json_data)
except ValueError:
message = 'Unable to parse JSON.'
self.send_error(400, message=message) # Bad Request
# Set up response dictionary.
self.response = dict()
def set_default_headers(self):
self.set_header('Content-Type', 'application/json')
def write_error(self, status_code, **kwargs):
if 'message' not in kwargs:
if status_code == 405:
kwargs['message'] = 'Invalid HTTP method.'
else:
kwargs['message'] = 'Unknown error.'
self.response = kwargs
self.write_json()
def write_json(self):
output = json.dumps(self.response)
self.write(output)
@pyghassen
Copy link

Can put an example how to use it?

@mminer
Copy link
Author

mminer commented Apr 24, 2014

No problem. You want your request handler to inherit from JsonHandler. You then access the JSON request body using the self.request.arguments dict and modify the response JSON using the self.response dict.

class MyHandler(JsonHandler):
    def get(self):
        # Access JSON request body through a dict.
        incoming_arg_1 = self.request.arguments['some_arg_1']
        incoming_arg_2 = self.request.arguments['some_arg_2']

        # Do something with arguments...

        # Set JSON response body through a dict.
        self.response['outgoing_arg_1'] = 'Response text'
        self.response['outgoing_arg_2'] = 12345

        # Required at end of method (similar to self.write).
        self.write_json()

The response will be properly encoded as JSON with the appropriate Content-Type header set. Additionally, if the request body is invalid JSON, the appropriate error code will be sent to the client.

Hope that helps!

@pconerly
Copy link

There's a bug in this-- tornado expects the argument dictionary to be in the form of:

{
key: ['list', 'of', 'values'],
key: ['list', 'of', 'values'],
}

Even if there's only one value for each key. This is to handle situations where we can pass multiple key-value pairs with the same key into a url querypath.

So if you post with {'message':'derp'} and use the built-in tornado function for getting arguments, you'll get this:

message = self.get_argument('message')
print message
> 'p'

This is because tornado always gives you the last argument. -_-;

Additionally I pop the raw json out of the arguments dictionary. In the original version it stays there and it's ugly.

Here's my version of prepare()

    def prepare(self):
        '''Incorporate request JSON into arguments dictionary.'''
        if self.request.body:
            try:
                json_data = json.loads(self.request.body)
                for k, v in json_data.items():
                    # Tornado expects values in the argument dict to be lists.
                    # in tornado.web.RequestHandler._get_argument the last argument is returned.
                    json_data[k] = [v]
                self.request.arguments.pop(self.request.body)
                self.request.arguments.update(json_data)
            except ValueError, e:
                logger.debug(e.message)
                message = 'Unable to parse JSON.'
                self.send_error(400, message=message) # Bad Request

        # Set up response dictionary.
        self.response = dict()

But overall: @mminer this is awesome and thanks for publishing this! As a tornado-noob it helped me a bunch.

@benjaminwilson
Copy link

Thanks for this. I am confused about the pop()? The body shouldn't be a key in the arguments dict. Also the logger fails without an import.

@JohnBrodie
Copy link

In Python 3, you might want to do json_data = tornado.escape.json_decode(self.request.body). self.request.body returns bytes in Python 3, and json.loads will not handle it automatically.

Thanks for the code.

@amitripshtos
Copy link

@pconerly , The problem is that a number/float variables will not work well using your method.

@weaming
Copy link

weaming commented May 30, 2016

@JohnBrodie Help me so much, but still no work on python3

This works for me:

for k,v in self.request.body_arguments.items():
    body_kw[k] = str(v[0], encoding='utf-8')
    if body_kw[k] == 'true':
        body_kw[k] = True
    if body_kw[k] == 'false':
        body_kw[k] = False
print(body_kw)

@root42
Copy link

root42 commented Dec 1, 2017

Also note, that BaseHandler needs to be defined. Instead one can probably use tornado.web.RequestHandler.

@AlexanderSov
Copy link

From documentation: "write() is used for non-template-based output; it accepts strings, bytes, and dictionaries (dicts will be encoded as JSON)." So, may be better send self.response to self.write()?
One moment more is about writing json_data into self.request.arguments. Also like in documentation, may be better write its in self.json_args and use tornado.escape.json_decode(self.request.body) instead json.loads(self.request.body)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment