Skip to content

Commit 813ac5d

Browse files
committed
let the user choose a custom json serializer through the RESTPLUS_JSON_SERIALIZER config option
1 parent bee950a commit 813ac5d

File tree

5 files changed

+143
-1
lines changed

5 files changed

+143
-1
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Changelog
66
Current
77
-------
88

9+
- **Behavior**:
10+
* ``ujson`` is not the default json serializer anymore. A new configuration option is available instead: ``RESTPLUS_JSON_SERIALIZER`` (:issue:`507`, :issue:`587`, :issue:`589`, :pr:`637`)
911
- Add new `Wildcard` fields (:pr:`255`)
1012
- Fix ABC deprecation warnings (:pr:`580`)
1113
- Fix `@api.expect(..., validate=False)` decorators for an :class:`Api` where `validate=True` is set on the constructor (:issue:`609`, :pr:`610`)

doc/quickstart.rst

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,75 @@ parameter to some classes or function to force order preservation:
308308
- globally on :class:`Namespace`: ``ns = Namespace(ordered=True)``
309309
- locally on :func:`marshal`: ``return marshal(data, fields, ordered=True)``
310310

311+
Configuration
312+
-------------
313+
314+
The following configuration options exist for Flask-RESTPlus:
315+
316+
============================ =============== ==================================
317+
OPTION DEFAULT VALUE DESCRIPTION
318+
============================ =============== ==================================
319+
``BUNDLE_ERRORS`` ``False`` Bundle all the validation errors
320+
instead of returning only the
321+
first one encountered.
322+
See the `Error Handling
323+
<parsing.html#error-handling>`__
324+
section of the documentation for
325+
details.
326+
``RESTPLUS_VALIDATE`` ``False`` Whether to enforce payload
327+
validation by default when using
328+
the ``@api.expect()`` decorator.
329+
See the `@api.expect()
330+
<swagger.html#the-api-expect-decorator>`__
331+
documentation for details.
332+
``RESTPLUS_MASK_HEADER`` ``X-Fields`` Choose the name of the *Header*
333+
that will contain the masks to
334+
apply to your answer.
335+
See the `Fields masks <mask.html>`__
336+
documentation for details.
337+
``RESTPLUS_MASK_SWAGGER`` ``True`` Whether to enable the mask
338+
documentation in your swagger or
339+
not.
340+
See the `mask usage
341+
<mask.html#usage>`__ documentation
342+
for details.
343+
``RESTPLUS_JSON`` ``{}`` Dictionary of options to pass to
344+
the json *serializer* (by default
345+
``json.dumps``).
346+
``RESTPLUS_JSON_SERIALIZER`` ``None`` Here you can choose your
347+
own/preferred json *serializer*.
348+
You can either specify the name
349+
of the module (example: ``ujson``)
350+
or you can give the full name of
351+
your *serializer* (example:
352+
``ujson.dumps``).
353+
354+
.. note::
355+
If you only specify the module
356+
name the default Flask-RESTPlus
357+
behavior is to import its
358+
``dumps`` method.
359+
360+
361+
.. note::
362+
Flask-RESTPlus will always
363+
silently fallback to the
364+
default ``json.dumps``
365+
*serializer* if it cannot
366+
manage to import the one
367+
you configured.
368+
369+
370+
.. warning::
371+
We only officially support
372+
python's builtin
373+
``json.dumps``.
374+
Please keep in mind some
375+
serializers may behave
376+
differently depending on the
377+
input types (floats, dates,
378+
etc.).
379+
============================ =============== ==================================
311380

312381
Full example
313382
------------

flask_restplus/representations.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,44 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import unicode_literals, absolute_import
33

4+
import importlib
5+
46
from json import dumps
57

68
from flask import make_response, current_app
79

10+
DEFAULT_SERIALIZER = 'dumps'
11+
serializer = None
12+
13+
14+
def _importer(mod_name, func_name=DEFAULT_SERIALIZER, default=dumps):
15+
imported = importlib.import_module(mod_name)
16+
return getattr(imported, func_name, default)
17+
818

919
def output_json(data, code, headers=None):
1020
'''Makes a Flask response with a JSON encoded body'''
1121

22+
global serializer
23+
1224
settings = current_app.config.get('RESTPLUS_JSON', {})
25+
custom_serializer = current_app.config.get('RESTPLUS_JSON_SERIALIZER', None)
26+
27+
# If the user wants to use a custom serializer, let it be
28+
if serializer is None and custom_serializer:
29+
try:
30+
serializer = _importer(custom_serializer)
31+
except ImportError:
32+
if '.' in custom_serializer:
33+
mod, func = custom_serializer.rsplit('.', 1)
34+
try:
35+
serializer = _importer(mod, func)
36+
except ImportError:
37+
pass
38+
39+
# fallback, no serializer found so far, use the default one
40+
if serializer is None:
41+
serializer = dumps
1342

1443
# If we're in debug mode, and the indent is not set, we set it to a
1544
# reasonable value here. Note that this won't override any existing value
@@ -19,7 +48,7 @@ def output_json(data, code, headers=None):
1948

2049
# always end the json dumps with a new line
2150
# see https://github.com/mitsuhiko/flask/pull/1262
22-
dumped = dumps(data, **settings) + "\n"
51+
dumped = serializer(data, **settings) + "\n"
2352

2453
resp = make_response(dumped, code)
2554
resp.headers.extend(headers or {})

requirements/test.pip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ pytest-mock==1.6.3
99
pytest-profiling==1.2.11
1010
pytest-sugar==0.9.0
1111
tzlocal
12+
ujson

tests/test_representations.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import flask_restplus.representations as rep
2+
3+
from json import dumps, loads
4+
from ujson import dumps as udumps, loads as uloads
5+
6+
payload = {
7+
'id': 1,
8+
'name': 'toto',
9+
'address': 'test',
10+
}
11+
12+
13+
def test_representations_serialization_output_correct(app):
14+
r = rep.output_json(payload, 200)
15+
assert loads(r.get_data(True)) == loads(dumps(payload))
16+
17+
18+
def test_config_custom_serializer_is_module(app):
19+
# now reset serializer
20+
rep.serializer = None
21+
# then enforce a custom serializer
22+
app.config['RESTPLUS_JSON_SERIALIZER'] = 'ujson'
23+
r2 = rep.output_json(payload, 200)
24+
assert uloads(r2.get_data(True)) == uloads(udumps(payload))
25+
assert rep.serializer == udumps
26+
27+
28+
def test_config_custom_serializer_is_function(app):
29+
# test other config syntax
30+
rep.serializer = None
31+
app.config['RESTPLUS_JSON_SERIALIZER'] = 'ujson.dumps'
32+
rep.output_json(payload, 200)
33+
assert rep.serializer == udumps
34+
35+
36+
def test_config_custom_serializer_fallback(app):
37+
# test fallback
38+
rep.serializer = None
39+
app.config['RESTPLUS_JSON_SERIALIZER'] = 'ujson.lol.dumps'
40+
rep.output_json(payload, 200)
41+
assert rep.serializer == dumps

0 commit comments

Comments
 (0)