pythonjsondatetimebottlebson

Bottle framework: how to return datetime in JSON response


When I try to return JSON containing datetime value, I'm getting

  File "/usr/lib/python2.7/json/encoder.py", line 178, in default
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: datetime.datetime(2014, 2, 1, 0, 0) is not JSON serializable

Which is normal. Is there an easy way to add an object hook to bottle like

from bson import json_util
import json
json.dumps(anObject, default=json_util.default)

to get datetime values converted?


Solution

  • Interesting question! I can see a couple of ways of doing this. The one would be to write a custom plugin that wraps the JSONPlugin:

    from bottle import route, run, install, JSONPlugin
    from bson import json_util
    
    class JSONDefaultPlugin(JSONPlugin):
        def __init__(self):
            super(JSONDefaultPlugin, self).__init__()
            self.plain_dump = self.json_dumps
            self.json_dumps = lambda body: self.plain_dump(body, default=json_util.default)
    

    Which can then be used like this:

    @route('/hello')
    def index(name):
        return {'test': datetime.datetime(2014, 2, 1, 0, 0)}
    
    install(JSONDefaultPlugin())
    run(host='localhost', port=8080)
    

    And will give output like this:

    {"test": {"$date": 1391212800000}}
    

    Another, shorter, way is to simply specify the json_loads parameter when instantiating the JSONPlugin class:

    import json
    from bson import json_util
    
    install(JSONPlugin(json_dumps=lambda body: json.dumps(body, default=json_util.default)))
    

    This produces the same result.

    Background

    This all makes a little more sense when you look at the source code for bottle (some parts removed below for brevity):

    class JSONPlugin(object):
        name = 'json'
        api  = 2
    
        def __init__(self, json_dumps=json_dumps):
            self.json_dumps = json_dumps
    
        def apply(self, callback, route):
            dumps = self.json_dumps
            if not dumps: return callback
            def wrapper(*a, **ka):
                ... 
    
                if isinstance(rv, dict):
                    ...
                elif isinstance(rv, HTTPResponse) and isinstance(rv.body, dict):
                    rv.body = dumps(rv.body)
                    rv.content_type = 'application/json'
                return rv
    
            return wrapper
    

    All we need to do is make sure the call to dumps there receives the default keyword argument you wish to provide.