diff --git a/docs/index.rst b/docs/index.rst
index 2b1fd96..bea0328 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -81,7 +81,9 @@ Flask-PyMongo provides helpers for some common tasks:
.. automethod:: flask_pymongo.PyMongo.save_file
-.. autoclass:: flask_pymongo.BSONObjectIdConverter
+.. autoclass:: flask_pymongo.helpers.BSONObjectIdConverter
+
+.. autoclass:: flask_pymongo.helpers.JSONEncoder
Configuration
-------------
@@ -92,6 +94,13 @@ You can configure Flask-PyMongo either by passing a `MongoDB URI
``MONGO_URI`` `Flask configuration variable
`_
+The :class:`~flask_pymongo.PyMongo` instnace also accepts these additional
+customization options:
+
+* ``json_options``, a :class:`~bson.json_util.JSONOptions` instance which
+ controls the JSON serialization of MongoDB objects when used with
+ :func:`~flask.json.jsonify`.
+
You may also pass additional keyword arguments to the ``PyMongo``
constructor. These are passed directly through to the underlying
:class:`~pymongo.mongo_client.MongoClient` object.
@@ -179,6 +188,8 @@ Changes:
MongoDB 3.2 support.
- `#130 `_ Fix
quickstart example in README (Emmanuel Arias).
+ - `#62 `_ Add
+ support for :func:`~flask.json.jsonify()`.
- 2.3.0: April 24, 2019
diff --git a/flask_pymongo/__init__.py b/flask_pymongo/__init__.py
index 1b3200c..896c485 100644
--- a/flask_pymongo/__init__.py
+++ b/flask_pymongo/__init__.py
@@ -26,18 +26,17 @@
__all__ = ("PyMongo", "ASCENDING", "DESCENDING")
+from functools import partial
from mimetypes import guess_type
import sys
-from bson.errors import InvalidId
-from bson.objectid import ObjectId
from flask import abort, current_app, request
from gridfs import GridFS, NoFile
from pymongo import uri_parser
-from werkzeug.routing import BaseConverter
from werkzeug.wsgi import wrap_file
import pymongo
+from flask_pymongo.helpers import BSONObjectIdConverter, JSONEncoder
from flask_pymongo.wrappers import MongoClient
@@ -59,35 +58,6 @@
"""Ascending sort order."""
-class BSONObjectIdConverter(BaseConverter):
-
- """A simple converter for the RESTful URL routing system of Flask.
-
- .. code-block:: python
-
- @app.route("/")
- def show_task(task_id):
- task = mongo.db.tasks.find_one_or_404(task_id)
- return render_template("task.html", task=task)
-
- Valid object ID strings are converted into
- :class:`~bson.objectid.ObjectId` objects; invalid strings result
- in a 404 error. The converter is automatically registered by the
- initialization of :class:`~flask_pymongo.PyMongo` with keyword
- :attr:`ObjectId`.
-
- """
-
- def to_python(self, value):
- try:
- return ObjectId(value)
- except InvalidId:
- raise abort(404)
-
- def to_url(self, value):
- return str(value)
-
-
class PyMongo(object):
"""Manages MongoDB connections for your Flask app.
@@ -102,9 +72,10 @@ class PyMongo(object):
"""
- def __init__(self, app=None, uri=None, *args, **kwargs):
+ def __init__(self, app=None, uri=None, json_options=None, *args, **kwargs):
self.cx = None
self.db = None
+ self._json_encoder = partial(JSONEncoder, json_options=json_options)
if app is not None:
self.init_app(app, uri, *args, **kwargs)
@@ -156,6 +127,7 @@ def init_app(self, app, uri=None, *args, **kwargs):
self.db = self.cx[database_name]
app.url_map.converters["ObjectId"] = BSONObjectIdConverter
+ app.json_encoder = self._json_encoder
# view helpers
def send_file(self, filename, base="fs", version=-1, cache_for=31536000):
diff --git a/flask_pymongo/helpers.py b/flask_pymongo/helpers.py
new file mode 100644
index 0000000..b1fb4ea
--- /dev/null
+++ b/flask_pymongo/helpers.py
@@ -0,0 +1,154 @@
+# Copyright (c) 2011-2019, Dan Crosta
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+
+__all__ = ("BSONObjectIdConverter", "JSONEncoder")
+
+from bson import json_util, SON
+from bson.errors import InvalidId
+from bson.objectid import ObjectId
+from flask import abort, json as flask_json
+from six import iteritems, string_types
+from werkzeug.routing import BaseConverter
+import pymongo
+
+if pymongo.version_tuple >= (3, 5, 0):
+ from bson.json_util import RELAXED_JSON_OPTIONS
+ DEFAULT_JSON_OPTIONS = RELAXED_JSON_OPTIONS
+else:
+ DEFAULT_JSON_OPTIONS = None
+
+
+def _iteritems(obj):
+ if hasattr(obj, "iteritems"):
+ return obj.iteritems()
+ elif hasattr(obj, "items"):
+ return obj.items()
+ else:
+ raise TypeError("{!r} missing iteritems() and items()".format(obj))
+
+
+class BSONObjectIdConverter(BaseConverter):
+
+ """A simple converter for the RESTful URL routing system of Flask.
+
+ .. code-block:: python
+
+ @app.route("/")
+ def show_task(task_id):
+ task = mongo.db.tasks.find_one_or_404(task_id)
+ return render_template("task.html", task=task)
+
+ Valid object ID strings are converted into
+ :class:`~bson.objectid.ObjectId` objects; invalid strings result
+ in a 404 error. The converter is automatically registered by the
+ initialization of :class:`~flask_pymongo.PyMongo` with keyword
+ :attr:`ObjectId`.
+
+ The :class:`~flask_pymongo.helpers.BSONObjectIdConverter` is
+ automatically installed on the :class:`~flask_pymongo.PyMongo`
+ instnace at creation time.
+
+ """
+
+ def to_python(self, value):
+ try:
+ return ObjectId(value)
+ except InvalidId:
+ raise abort(404)
+
+ def to_url(self, value):
+ return str(value)
+
+
+class JSONEncoder(flask_json.JSONEncoder):
+
+ """A JSON encoder that uses :mod:`bson.json_util` for MongoDB documents.
+
+ .. code-block:: python
+
+ @app.route("/cart/")
+ def json_route(cart_id):
+ results = mongo.db.carts.find({"_id": cart_id})
+ return jsonify(results)
+
+ # returns a Response with JSON body and application/json content-type:
+ # '[{"count":12,"item":"egg"},{"count":1,"item":"apple"}]'
+
+ Since this uses PyMongo's JSON tools, certain types may serialize
+ differently than you expect. See :class:`~bson.json_util.JSONOptions`
+ for details on the particular serialization that will be used.
+
+ A :class:`~flask_pymongo.helpers.JSONEncoder` is automatically
+ automatically installed on the :class:`~flask_pymongo.PyMongo`
+ instance at creation time, using
+ :const:`~bson.json_util.RELAXED_JSON_OPTIONS`. You can change the
+ :class:`~bson.json_util.JSONOptions` in use by passing
+ ``json_options`` to the :class:`~flask_pymongo.PyMongo`
+ constructor.
+
+ .. note::
+
+ :class:`~bson.json_util.JSONOptions` is only supported as of
+ PyMongo version 3.4. For older versions of PyMongo, you will
+ have less control over the JSON format that results from calls
+ to :func:`~flask.json.jsonify`.
+
+ .. versionadded:: 2.4.0
+
+ """
+
+ def __init__(self, json_options, *args, **kwargs):
+ if json_options is None:
+ json_options = DEFAULT_JSON_OPTIONS
+ if json_options is not None:
+ self._default_kwargs = {"json_options": json_options}
+ else:
+ self._default_kwargs = {}
+
+ super(JSONEncoder, self).__init__(*args, **kwargs)
+
+ def default(self, obj):
+ """Serialize MongoDB object types using :mod:`bson.json_util`.
+
+ Falls back to Flask's default JSON serialization for all other types.
+
+ This may raise ``TypeError`` for object types not recignozed.
+
+ .. versionadded:: 2.4.0
+
+ """
+ if hasattr(obj, "iteritems") or hasattr(obj, "items"):
+ return SON((k, self.default(v)) for k, v in iteritems(obj))
+ elif hasattr(obj, "__iter__") and not isinstance(obj, string_types):
+ return [self.default(v) for v in obj]
+ else:
+ try:
+ return json_util.default(obj, **self._default_kwargs)
+ except TypeError:
+ # PyMongo couldn't convert into a serializable object, and
+ # the Flask default JSONEncoder won't; so we return the
+ # object itself and let stdlib json handle it if possible
+ return obj
diff --git a/flask_pymongo/tests/test_json.py b/flask_pymongo/tests/test_json.py
new file mode 100644
index 0000000..93d179d
--- /dev/null
+++ b/flask_pymongo/tests/test_json.py
@@ -0,0 +1,29 @@
+import json
+
+from bson import ObjectId
+from flask import jsonify
+from six import ensure_str
+
+from flask_pymongo.tests.util import FlaskPyMongoTest
+
+
+class JSONTest(FlaskPyMongoTest):
+
+ def test_it_encodes_json(self):
+ resp = jsonify({"foo": "bar"})
+ dumped = json.loads(ensure_str(resp.get_data()))
+ self.assertEqual(dumped, {"foo": "bar"})
+
+ def test_it_handles_pymongo_types(self):
+ resp = jsonify({"id": ObjectId("5cf29abb5167a14c9e6e12c4")})
+ dumped = json.loads(ensure_str(resp.get_data()))
+ self.assertEqual(dumped, {"id": {"$oid": "5cf29abb5167a14c9e6e12c4"}})
+
+ def test_it_jsonifies_a_cursor(self):
+ self.mongo.db.rows.insert_many([{"foo": "bar"}, {"foo": "baz"}])
+
+ curs = self.mongo.db.rows.find(projection={"_id": False}).sort("foo")
+
+ resp = jsonify(curs)
+ dumped = json.loads(ensure_str(resp.get_data()))
+ self.assertEqual([{"foo": "bar"}, {"foo": "baz"}], dumped)
diff --git a/setup.py b/setup.py
index cb44097..a1d04bd 100644
--- a/setup.py
+++ b/setup.py
@@ -32,6 +32,7 @@
install_requires=[
"Flask>=0.11",
"PyMongo>=3.3",
+ "six",
],
classifiers=[
"Environment :: Web Environment",