add json provider interface

This commit is contained in:
David Lord 2022-07-13 07:41:43 -07:00
parent c356c6da5f
commit 69f9845ef2
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
13 changed files with 662 additions and 282 deletions

View file

@ -32,7 +32,22 @@ Unreleased
``Flask.aborter_class`` and ``Flask.make_aborter`` can be used
to customize this aborter. :issue:`4567`
- ``flask.redirect`` will call ``app.redirect``. :issue:`4569`
- ``flask.json`` is an instance of ``JSONProvider``. A different
provider can be set to use a different JSON library.
``flask.jsonify`` will call ``app.json.response``, other
functions in ``flask.json`` will call corresponding functions in
``app.json``. :pr:`4688`
- JSON configuration is moved to attributes on the default
``app.json`` provider. ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``,
``JSONIFY_MIMETYPE``, and ``JSONIFY_PRETTYPRINT_REGULAR`` are
deprecated. :pr:`4688`
- Setting custom ``json_encoder`` and ``json_decoder`` classes on the
app or a blueprint, and the corresponding ``json.JSONEncoder`` and
``JSONDecoder`` classes, are deprecated. JSON behavior can now be
overridden using the ``app.json`` provider interface. :pr:`4688`
- ``json.htmlsafe_dumps`` and ``json.htmlsafe_dump`` are deprecated,
the function is built-in to Jinja now. :pr:`4688`
- Refactor ``register_error_handler`` to consolidate error checking.
Rewrite some error messages to be more consistent. :issue:`4559`
- Use Blueprint decorators and functions intended for setup after

View file

@ -236,21 +236,15 @@ JSON Support
.. module:: flask.json
Flask uses the built-in :mod:`json` module for handling JSON. It will
use the current blueprint's or application's JSON encoder and decoder
for easier customization. By default it handles some extra data types:
Flask uses Python's built-in :mod:`json` module for handling JSON by
default. The JSON implementation can be changed by assigning a different
provider to :attr:`flask.Flask.json_provider_class` or
:attr:`flask.Flask.json`. The functions provided by ``flask.json`` will
use methods on ``app.json`` if an app context is active.
- :class:`datetime.datetime` and :class:`datetime.date` are serialized
to :rfc:`822` strings. This is the same as the HTTP date format.
- :class:`uuid.UUID` is serialized to a string.
- :class:`dataclasses.dataclass` is passed to
:func:`dataclasses.asdict`.
- :class:`~markupsafe.Markup` (or any object with a ``__html__``
method) will call the ``__html__`` method to get a string.
Jinja's ``|tojson`` filter is configured to use Flask's :func:`dumps`
function. The filter marks the output with ``|safe`` automatically. Use
the filter to render data inside ``<script>`` tags.
Jinja's ``|tojson`` filter is configured to use the app's JSON provider.
The filter marks the output with ``|safe``. Use it to render data inside
HTML ``<script>`` tags.
.. sourcecode:: html+jinja
@ -269,6 +263,14 @@ the filter to render data inside ``<script>`` tags.
.. autofunction:: load
.. autoclass:: flask.json.provider.JSONProvider
:members:
:member-order: bysource
.. autoclass:: flask.json.provider.DefaultJSONProvider
:members:
:member-order: bysource
.. autoclass:: JSONEncoder
:members:

View file

@ -301,6 +301,10 @@ The following configuration values are used internally by Flask:
Default: ``True``
.. deprecated:: 2.2
Will be removed in Flask 2.3. Set ``app.json.ensure_ascii``
instead.
.. py:data:: JSON_SORT_KEYS
Sort the keys of JSON objects alphabetically. This is useful for caching
@ -310,19 +314,30 @@ The following configuration values are used internally by Flask:
Default: ``True``
.. deprecated:: 2.2
Will be removed in Flask 2.3. Set ``app.json.sort_keys``
instead.
.. py:data:: JSONIFY_PRETTYPRINT_REGULAR
``jsonify`` responses will be output with newlines, spaces, and indentation
for easier reading by humans. Always enabled in debug mode.
:func:`~flask.jsonify` responses will be output with newlines,
spaces, and indentation for easier reading by humans. Always enabled
in debug mode.
Default: ``False``
.. deprecated:: 2.2
Will be removed in Flask 2.3. Set ``app.json.compact`` instead.
.. py:data:: JSONIFY_MIMETYPE
The mimetype of ``jsonify`` responses.
Default: ``'application/json'``
.. deprecated:: 2.2
Will be removed in Flask 2.3. Set ``app.json.mimetype`` instead.
.. py:data:: TEMPLATES_AUTO_RELOAD
Reload templates when they are changed. If not set, it will be enabled in
@ -387,6 +402,12 @@ The following configuration values are used internally by Flask:
.. versionchanged:: 2.2
Removed ``PRESERVE_CONTEXT_ON_EXCEPTION``.
.. versionchanged:: 2.2
``JSON_AS_ASCII``, ``JSON_SORT_KEYS``,
``JSONIFY_MIMETYPE``, and ``JSONIFY_PRETTYPRINT_REGULAR`` will be
removed in Flask 2.3. The default ``app.json`` provider has
equivalent attributes instead.
Configuring from Python Files
-----------------------------

View file

@ -31,7 +31,6 @@ from werkzeug.utils import redirect as _wz_redirect
from werkzeug.wrappers import Response as BaseResponse
from . import cli
from . import json
from . import typing as ft
from .config import Config
from .config import ConfigAttribute
@ -50,7 +49,8 @@ from .helpers import get_env
from .helpers import get_flashed_messages
from .helpers import get_load_dotenv
from .helpers import locked_cached_property
from .json import jsonify
from .json.provider import DefaultJSONProvider
from .json.provider import JSONProvider
from .logging import create_logger
from .scaffold import _endpoint_from_view_func
from .scaffold import _sentinel
@ -315,15 +315,37 @@ class Flask(Scaffold):
#: ``USE_X_SENDFILE`` configuration key. Defaults to ``False``.
use_x_sendfile = ConfigAttribute("USE_X_SENDFILE")
#: The JSON encoder class to use. Defaults to :class:`~flask.json.JSONEncoder`.
#: The JSON encoder class to use. Defaults to
#: :class:`~flask.json.JSONEncoder`.
#:
#: .. deprecated:: 2.2
#: Will be removed in Flask 2.3. Customize
#: :attr:`json_provider_class` instead.
#:
#: .. versionadded:: 0.10
json_encoder = json.JSONEncoder
json_encoder: None = None
#: The JSON decoder class to use. Defaults to :class:`~flask.json.JSONDecoder`.
#: The JSON decoder class to use. Defaults to
#: :class:`~flask.json.JSONDecoder`.
#:
#: .. deprecated:: 2.2
#: Will be removed in Flask 2.3. Customize
#: :attr:`json_provider_class` instead.
#:
#: .. versionadded:: 0.10
json_decoder = json.JSONDecoder
json_decoder: None = None
json_provider_class: t.Type[JSONProvider] = DefaultJSONProvider
"""A subclass of :class:`~flask.json.provider.JSONProvider`. An
instance is created and assigned to :attr:`app.json` when creating
the app.
The default, :class:`~flask.json.provider.DefaultJSONProvider`, uses
Python's built-in :mod:`json` library. A different provider can use
a different JSON library.
.. versionadded:: 2.2
"""
#: Options that are passed to the Jinja environment in
#: :meth:`create_jinja_environment`. Changing these options after
@ -361,10 +383,10 @@ class Flask(Scaffold):
"TRAP_HTTP_EXCEPTIONS": False,
"EXPLAIN_TEMPLATE_LOADING": False,
"PREFERRED_URL_SCHEME": "http",
"JSON_AS_ASCII": True,
"JSON_SORT_KEYS": True,
"JSONIFY_PRETTYPRINT_REGULAR": False,
"JSONIFY_MIMETYPE": "application/json",
"JSON_AS_ASCII": None,
"JSON_SORT_KEYS": None,
"JSONIFY_PRETTYPRINT_REGULAR": None,
"JSONIFY_MIMETYPE": None,
"TEMPLATES_AUTO_RELOAD": None,
"MAX_COOKIE_SIZE": 4093,
}
@ -449,6 +471,22 @@ class Flask(Scaffold):
#: Moved from ``flask.abort``, which calls this object.
self.aborter = self.make_aborter()
self.json: JSONProvider = self.json_provider_class(self)
"""Provides access to JSON methods. Functions in ``flask.json``
will call methods on this provider when the application context
is active. Used for handling JSON requests and responses.
An instance of :attr:`json_provider_class`. Can be customized by
changing that attribute on a subclass, or by assigning to this
attribute afterwards.
The default, :class:`~flask.json.provider.DefaultJSONProvider`,
uses Python's built-in :mod:`json` library. A different provider
can use a different JSON library.
.. versionadded:: 2.2
"""
#: A list of functions that are called by
#: :meth:`handle_url_build_error` when :meth:`.url_for` raises a
#: :exc:`~werkzeug.routing.BuildError`. Each function is called
@ -745,7 +783,7 @@ class Flask(Scaffold):
session=session,
g=g,
)
rv.policies["json.dumps_function"] = json.dumps
rv.policies["json.dumps_function"] = self.json.dumps
return rv
def create_global_jinja_loader(self) -> DispatchingJinjaLoader:
@ -1926,7 +1964,7 @@ class Flask(Scaffold):
)
status = headers = None
elif isinstance(rv, (dict, list)):
rv = jsonify(rv)
rv = self.json.response(rv)
elif isinstance(rv, BaseResponse) or callable(rv):
# evaluate a WSGI callable, or coerce a different response
# class to the correct type

View file

@ -174,10 +174,16 @@ class Blueprint(Scaffold):
#: Blueprint local JSON encoder class to use. Set to ``None`` to use
#: the app's :class:`~flask.Flask.json_encoder`.
json_encoder = None
#:
#: .. deprecated:: 2.2
#: Will be removed in Flask 2.3.
json_encoder: None = None
#: Blueprint local JSON decoder class to use. Set to ``None`` to use
#: the app's :class:`~flask.Flask.json_decoder`.
json_decoder = None
#:
#: .. deprecated:: 2.2
#: Will be removed in Flask 2.3.
json_decoder: None = None
def __init__(
self,

View file

@ -307,6 +307,7 @@ class RequestContext:
self.app = app
if request is None:
request = app.request_class(environ)
request.json_module = app.json # type: ignore[misc]
self.request: Request = request
self.url_adapter = None
try:

View file

@ -1,15 +1,12 @@
import dataclasses
import decimal
from __future__ import annotations
import json as _json
import typing as t
import uuid
from datetime import date
from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps
from werkzeug.http import http_date
from ..globals import current_app
from ..globals import request
from .provider import _default
if t.TYPE_CHECKING: # pragma: no cover
from ..app import Flask
@ -31,23 +28,30 @@ class JSONEncoder(_json.JSONEncoder):
Assign a subclass of this to :attr:`flask.Flask.json_encoder` or
:attr:`flask.Blueprint.json_encoder` to override the default.
.. deprecated:: 2.2
Will be removed in Flask 2.3. Use ``app.json`` instead.
"""
def __init__(self, **kwargs) -> None:
import warnings
warnings.warn(
"'JSONEncoder' is deprecated and will be removed in"
" Flask 2.3. Use 'Flask.json' to provide an alternate"
" JSON implementation instead.",
DeprecationWarning,
stacklevel=3,
)
super().__init__(**kwargs)
def default(self, o: t.Any) -> t.Any:
"""Convert ``o`` to a JSON serializable type. See
:meth:`json.JSONEncoder.default`. Python does not support
overriding how basic types like ``str`` or ``list`` are
serialized, they are handled before this method.
"""
if isinstance(o, date):
return http_date(o)
if isinstance(o, (decimal.Decimal, uuid.UUID)):
return str(o)
if dataclasses and dataclasses.is_dataclass(o):
return dataclasses.asdict(o)
if hasattr(o, "__html__"):
return str(o.__html__())
return super().default(o)
return _default(o)
class JSONDecoder(_json.JSONDecoder):
@ -58,144 +62,193 @@ class JSONDecoder(_json.JSONDecoder):
Assign a subclass of this to :attr:`flask.Flask.json_decoder` or
:attr:`flask.Blueprint.json_decoder` to override the default.
.. deprecated:: 2.2
Will be removed in Flask 2.3. Use ``app.json`` instead.
"""
def __init__(self, **kwargs) -> None:
import warnings
def _dump_arg_defaults(
kwargs: t.Dict[str, t.Any], app: t.Optional["Flask"] = None
) -> None:
"""Inject default arguments for dump functions."""
if app is None:
app = current_app
if app:
cls = app.json_encoder
bp = app.blueprints.get(request.blueprint) if request else None # type: ignore
if bp is not None and bp.json_encoder is not None:
cls = bp.json_encoder
# Only set a custom encoder if it has custom behavior. This is
# faster on PyPy.
if cls is not _json.JSONEncoder:
kwargs.setdefault("cls", cls)
kwargs.setdefault("cls", cls)
kwargs.setdefault("ensure_ascii", app.config["JSON_AS_ASCII"])
kwargs.setdefault("sort_keys", app.config["JSON_SORT_KEYS"])
else:
kwargs.setdefault("sort_keys", True)
kwargs.setdefault("cls", JSONEncoder)
warnings.warn(
"'JSONDecoder' is deprecated and will be removed in"
" Flask 2.3. Use 'Flask.json' to provide an alternate"
" JSON implementation instead.",
DeprecationWarning,
stacklevel=3,
)
super().__init__(**kwargs)
def _load_arg_defaults(
kwargs: t.Dict[str, t.Any], app: t.Optional["Flask"] = None
) -> None:
"""Inject default arguments for load functions."""
if app is None:
app = current_app
def dumps(obj: t.Any, *, app: Flask | None = None, **kwargs: t.Any) -> str:
"""Serialize data as JSON.
if app:
cls = app.json_decoder
bp = app.blueprints.get(request.blueprint) if request else None # type: ignore
if bp is not None and bp.json_decoder is not None:
cls = bp.json_decoder
If :data:`~flask.current_app` is available, it will use its
:meth:`app.json.dumps() <flask.json.provider.JSONProvider.dumps>`
method, otherwise it will use :func:`json.dumps`.
# Only set a custom decoder if it has custom behavior. This is
# faster on PyPy.
if cls not in {JSONDecoder, _json.JSONDecoder}:
kwargs.setdefault("cls", cls)
:param obj: The data to serialize.
:param kwargs: Arguments passed to the ``dumps`` implementation.
.. versionchanged:: 2.2
Calls ``current_app.json.dumps``, allowing an app to override
the behavior.
def dumps(obj: t.Any, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> str:
"""Serialize an object to a string of JSON.
Takes the same arguments as the built-in :func:`json.dumps`, with
some defaults from application configuration.
:param obj: Object to serialize to JSON.
:param app: Use this app's config instead of the active app context
or defaults.
:param kwargs: Extra arguments passed to :func:`json.dumps`.
.. versionchanged:: 2.2
The ``app`` parameter will be removed in Flask 2.3.
.. versionchanged:: 2.0.2
:class:`decimal.Decimal` is supported by converting to a string.
.. versionchanged:: 2.0
``encoding`` is deprecated and will be removed in Flask 2.1.
``encoding`` will be removed in Flask 2.1.
.. versionchanged:: 1.0.3
``app`` can be passed directly, rather than requiring an app
context for configuration.
"""
_dump_arg_defaults(kwargs, app=app)
if app is not None:
import warnings
warnings.warn(
"The 'app' parameter is deprecated and will be removed in"
" Flask 2.3. Call 'app.json.dumps' directly instead.",
DeprecationWarning,
stacklevel=2,
)
else:
app = current_app
if app:
return app.json.dumps(obj, **kwargs)
kwargs.setdefault("default", _default)
return _json.dumps(obj, **kwargs)
def dump(
obj: t.Any, fp: t.IO[str], app: t.Optional["Flask"] = None, **kwargs: t.Any
obj: t.Any, fp: t.IO[str], *, app: Flask | None = None, **kwargs: t.Any
) -> None:
"""Serialize an object to JSON written to a file object.
"""Serialize data as JSON and write to a file.
Takes the same arguments as the built-in :func:`json.dump`, with
some defaults from application configuration.
If :data:`~flask.current_app` is available, it will use its
:meth:`app.json.dump() <flask.json.provider.JSONProvider.dump>`
method, otherwise it will use :func:`json.dump`.
:param obj: Object to serialize to JSON.
:param fp: File object to write JSON to.
:param app: Use this app's config instead of the active app context
or defaults.
:param kwargs: Extra arguments passed to :func:`json.dump`.
:param obj: The data to serialize.
:param fp: A file opened for writing text. Should use the UTF-8
encoding to be valid JSON.
:param kwargs: Arguments passed to the ``dump`` implementation.
.. versionchanged:: 2.2
Calls ``current_app.json.dump``, allowing an app to override
the behavior.
.. versionchanged:: 2.2
The ``app`` parameter will be removed in Flask 2.3.
.. versionchanged:: 2.0
Writing to a binary file, and the ``encoding`` argument, is
deprecated and will be removed in Flask 2.1.
Writing to a binary file, and the ``encoding`` argument, will be
removed in Flask 2.1.
"""
_dump_arg_defaults(kwargs, app=app)
_json.dump(obj, fp, **kwargs)
if app is not None:
import warnings
warnings.warn(
"The 'app' parameter is deprecated and will be removed in"
" Flask 2.3. Call 'app.json.dump' directly instead.",
DeprecationWarning,
stacklevel=2,
)
else:
app = current_app
if app:
app.json.dump(obj, fp, **kwargs)
else:
kwargs.setdefault("default", _default)
_json.dump(obj, fp, **kwargs)
def loads(
s: t.Union[str, bytes],
app: t.Optional["Flask"] = None,
**kwargs: t.Any,
) -> t.Any:
"""Deserialize an object from a string of JSON.
def loads(s: str | bytes, *, app: Flask | None = None, **kwargs: t.Any) -> t.Any:
"""Deserialize data as JSON.
Takes the same arguments as the built-in :func:`json.loads`, with
some defaults from application configuration.
If :data:`~flask.current_app` is available, it will use its
:meth:`app.json.loads() <flask.json.provider.JSONProvider.loads>`
method, otherwise it will use :func:`json.loads`.
:param s: JSON string to deserialize.
:param app: Use this app's config instead of the active app context
or defaults.
:param kwargs: Extra arguments passed to :func:`json.loads`.
:param s: Text or UTF-8 bytes.
:param kwargs: Arguments passed to the ``loads`` implementation.
.. versionchanged:: 2.2
Calls ``current_app.json.loads``, allowing an app to override
the behavior.
.. versionchanged:: 2.2
The ``app`` parameter will be removed in Flask 2.3.
.. versionchanged:: 2.0
``encoding`` is deprecated and will be removed in Flask 2.1. The
data must be a string or UTF-8 bytes.
``encoding`` will be removed in Flask 2.1. The data must be a
string or UTF-8 bytes.
.. versionchanged:: 1.0.3
``app`` can be passed directly, rather than requiring an app
context for configuration.
"""
_load_arg_defaults(kwargs, app=app)
if app is not None:
import warnings
warnings.warn(
"The 'app' parameter is deprecated and will be removed in"
" Flask 2.3. Call 'app.json.loads' directly instead.",
DeprecationWarning,
stacklevel=2,
)
else:
app = current_app
if app:
return app.json.loads(s, **kwargs)
return _json.loads(s, **kwargs)
def load(fp: t.IO[str], app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.Any:
"""Deserialize an object from JSON read from a file object.
def load(fp: t.IO[t.AnyStr], *, app: Flask | None = None, **kwargs: t.Any) -> t.Any:
"""Deserialize data as JSON read from a file.
Takes the same arguments as the built-in :func:`json.load`, with
some defaults from application configuration.
If :data:`~flask.current_app` is available, it will use its
:meth:`app.json.load() <flask.json.provider.JSONProvider.load>`
method, otherwise it will use :func:`json.load`.
:param fp: File object to read JSON from.
:param app: Use this app's config instead of the active app context
or defaults.
:param kwargs: Extra arguments passed to :func:`json.load`.
:param fp: A file opened for reading text or UTF-8 bytes.
:param kwargs: Arguments passed to the ``load`` implementation.
.. versionchanged:: 2.2
Calls ``current_app.json.load``, allowing an app to override
the behavior.
.. versionchanged:: 2.2
The ``app`` parameter will be removed in Flask 2.3.
.. versionchanged:: 2.0
``encoding`` is deprecated and will be removed in Flask 2.1. The
file must be text mode, or binary mode with UTF-8 bytes.
``encoding`` will be removed in Flask 2.1. The file must be text
mode, or binary mode with UTF-8 bytes.
"""
_load_arg_defaults(kwargs, app=app)
if app is not None:
import warnings
warnings.warn(
"The 'app' parameter is deprecated and will be removed in"
" Flask 2.3. Call 'app.json.load' directly instead.",
DeprecationWarning,
stacklevel=2,
)
else:
app = current_app
if app:
return app.json.load(fp, **kwargs)
return _json.load(fp, **kwargs)
@ -211,6 +264,9 @@ def htmlsafe_dumps(obj: t.Any, **kwargs: t.Any) -> str:
double quoted; either use single quotes or the ``|forceescape``
filter.
.. deprecated:: 2.2
Will be removed in Flask 2.3. This is built-in to Jinja now.
.. versionchanged:: 2.0
Uses :func:`jinja2.utils.htmlsafe_json_dumps`. The returned
value is marked safe by wrapping in :class:`~markupsafe.Markup`.
@ -220,6 +276,14 @@ def htmlsafe_dumps(obj: t.Any, **kwargs: t.Any) -> str:
``<script>`` tags, and single-quoted attributes without further
escaping.
"""
import warnings
warnings.warn(
"'htmlsafe_dumps' is deprecated and will be removed in Flask"
" 2.3. Use 'jinja2.utils.htmlsafe_json_dumps' instead.",
DeprecationWarning,
stacklevel=2,
)
return _jinja_htmlsafe_dumps(obj, dumps=dumps, **kwargs)
@ -227,77 +291,51 @@ def htmlsafe_dump(obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None:
"""Serialize an object to JSON written to a file object, replacing
HTML-unsafe characters with Unicode escapes. See
:func:`htmlsafe_dumps` and :func:`dumps`.
.. deprecated:: 2.2
Will be removed in Flask 2.3.
"""
import warnings
warnings.warn(
"'htmlsafe_dump' is deprecated and will be removed in Flask"
" 2.3. Use 'jinja2.utils.htmlsafe_json_dumps' instead.",
DeprecationWarning,
stacklevel=2,
)
fp.write(htmlsafe_dumps(obj, **kwargs))
def jsonify(*args: t.Any, **kwargs: t.Any) -> "Response":
"""Serialize data to JSON and wrap it in a :class:`~flask.Response`
with the :mimetype:`application/json` mimetype.
def jsonify(*args: t.Any, **kwargs: t.Any) -> Response:
"""Serialize the given arguments as JSON, and return a
:class:`~flask.Response` object with the ``application/json``
mimetype. A dict or list returned from a view will be converted to a
JSON response automatically without needing to call this.
Uses :func:`dumps` to serialize the data, but ``args`` and
``kwargs`` are treated as data rather than arguments to
:func:`json.dumps`.
This requires an active request or application context, and calls
:meth:`app.json.response() <flask.json.provider.JSONProvider.response>`.
1. Single argument: Treated as a single value.
2. Multiple arguments: Treated as a list of values.
``jsonify(1, 2, 3)`` is the same as ``jsonify([1, 2, 3])``.
3. Keyword arguments: Treated as a dict of values.
``jsonify(data=data, errors=errors)`` is the same as
``jsonify({"data": data, "errors": errors})``.
4. Passing both arguments and keyword arguments is not allowed as
it's not clear what should happen.
In debug mode, the output is formatted with indentation to make it
easier to read. This may also be controlled by the provider.
.. code-block:: python
Either positional or keyword arguments can be given, not both.
If no arguments are given, ``None`` is serialized.
from flask import jsonify
:param args: A single value to serialize, or multiple values to
treat as a list to serialize.
:param kwargs: Treat as a dict to serialize.
@app.route("/users/me")
def get_current_user():
return jsonify(
username=g.user.username,
email=g.user.email,
id=g.user.id,
)
Will return a JSON response like this:
.. code-block:: javascript
{
"username": "admin",
"email": "admin@localhost",
"id": 42
}
The default output omits indents and spaces after separators. In
debug mode or if :data:`JSONIFY_PRETTYPRINT_REGULAR` is ``True``,
the output will be formatted to be easier to read.
.. versionchanged:: 2.2
Calls ``current_app.json.response``, allowing an app to override
the behavior.
.. versionchanged:: 2.0.2
:class:`decimal.Decimal` is supported by converting to a string.
.. versionchanged:: 0.11
Added support for serializing top-level arrays. This introduces
a security risk in ancient browsers. See :ref:`security-json`.
Added support for serializing top-level arrays. This was a
security risk in ancient browsers. See :ref:`security-json`.
.. versionadded:: 0.2
"""
indent = None
separators = (",", ":")
if current_app.config["JSONIFY_PRETTYPRINT_REGULAR"] or current_app.debug:
indent = 2
separators = (", ", ": ")
if args and kwargs:
raise TypeError("jsonify() behavior undefined when passed both args and kwargs")
elif len(args) == 1: # single args are passed directly to dumps()
data = args[0]
else:
data = args or kwargs
return current_app.response_class(
f"{dumps(data, indent=indent, separators=separators)}\n",
mimetype=current_app.config["JSONIFY_MIMETYPE"],
)
return current_app.json.response(*args, **kwargs)

310
src/flask/json/provider.py Normal file
View file

@ -0,0 +1,310 @@
from __future__ import annotations
import dataclasses
import decimal
import json
import typing as t
import uuid
import weakref
from datetime import date
from werkzeug.http import http_date
from ..globals import request
if t.TYPE_CHECKING: # pragma: no cover
from ..app import Flask
from ..wrappers import Response
class JSONProvider:
"""A standard set of JSON operations for an application. Subclasses
of this can be used to customize JSON behavior or use different
JSON libraries.
To implement a provider for a specific library, subclass this base
class and implement at least :meth:`dumps` and :meth:`loads`. All
other methods have default implementations.
To use a different provider, either subclass ``Flask`` and set
:attr:`~flask.Flask.json_provider_class` to a provider class, or set
:attr:`app.json <flask.Flask.json>` to an instance of the class.
:param app: An application instance. This will be stored as a
:class:`weakref.proxy` on the :attr:`_app` attribute.
.. versionadded:: 2.2
"""
def __init__(self, app: Flask) -> None:
self._app = weakref.proxy(app)
def dumps(self, obj: t.Any, **kwargs: t.Any) -> str:
"""Serialize data as JSON.
:param obj: The data to serialize.
:param kwargs: May be passed to the underlying JSON library.
"""
raise NotImplementedError
def dump(self, obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None:
"""Serialize data as JSON and write to a file.
:param obj: The data to serialize.
:param fp: A file opened for writing text. Should use the UTF-8
encoding to be valid JSON.
:param kwargs: May be passed to the underlying JSON library.
"""
fp.write(self.dumps(obj, **kwargs))
def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any:
"""Deserialize data as JSON.
:param s: Text or UTF-8 bytes.
:param kwargs: May be passed to the underlying JSON library.
"""
raise NotImplementedError
def load(self, fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any:
"""Deserialize data as JSON read from a file.
:param fp: A file opened for reading text or UTF-8 bytes.
:param kwargs: May be passed to the underlying JSON library.
"""
return self.loads(fp.read(), **kwargs)
def _prepare_response_obj(
self, args: t.Tuple[t.Any, ...], kwargs: t.Dict[str, t.Any]
) -> t.Any:
if args and kwargs:
raise TypeError("app.json.response() takes either args or kwargs, not both")
if not args and not kwargs:
return None
if len(args) == 1:
return args[0]
return args or kwargs
def response(self, *args: t.Any, **kwargs: t.Any) -> Response:
"""Serialize the given arguments as JSON, and return a
:class:`~flask.Response` object with the ``application/json``
mimetype.
The :func:`~flask.json.jsonify` function calls this method for
the current application.
Either positional or keyword arguments can be given, not both.
If no arguments are given, ``None`` is serialized.
:param args: A single value to serialize, or multiple values to
treat as a list to serialize.
:param kwargs: Treat as a dict to serialize.
"""
obj = self._prepare_response_obj(args, kwargs)
return self._app.response_class(self.dumps(obj), mimetype="application/json")
def _default(o: t.Any) -> t.Any:
if isinstance(o, date):
return http_date(o)
if isinstance(o, (decimal.Decimal, uuid.UUID)):
return str(o)
if dataclasses and dataclasses.is_dataclass(o):
return dataclasses.asdict(o)
if hasattr(o, "__html__"):
return str(o.__html__())
raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
class DefaultJSONProvider(JSONProvider):
"""Provide JSON operations using Python's built-in :mod:`json`
library. Serializes the following additional data types:
- :class:`datetime.datetime` and :class:`datetime.date` are
serialized to :rfc:`822` strings. This is the same as the HTTP
date format.
- :class:`uuid.UUID` is serialized to a string.
- :class:`dataclasses.dataclass` is passed to
:func:`dataclasses.asdict`.
- :class:`~markupsafe.Markup` (or any object with a ``__html__``
method) will call the ``__html__`` method to get a string.
"""
default: t.Callable[[t.Any], t.Any] = staticmethod(
_default
) # type: ignore[assignment]
"""Apply this function to any object that :meth:`json.dumps` does
not know how to serialize. It should return a valid JSON type or
raise a ``TypeError``.
"""
ensure_ascii = True
"""Replace non-ASCII characters with escape sequences. This may be
more compatible with some clients, but can be disabled for better
performance and size.
"""
sort_keys = True
"""Sort the keys in any serialized dicts. This may be useful for
some caching situations, but can be disabled for better performance.
When enabled, keys must all be strings, they are not converted
before sorting.
"""
compact: bool | None = None
"""If ``True``, or ``None`` out of debug mode, the :meth:`response`
output will not add indentation, newlines, or spaces. If ``False``,
or ``None`` in debug mode, it will use a non-compact representation.
"""
mimetype = "application/json"
"""The mimetype set in :meth:`response`."""
def dumps(self, obj: t.Any, **kwargs: t.Any) -> str:
"""Serialize data as JSON to a string.
Keyword arguments are passed to :func:`json.dumps`. Sets some
parameter defaults from the :attr:`default`,
:attr:`ensure_ascii`, and :attr:`sort_keys` attributes.
:param obj: The data to serialize.
:param kwargs: Passed to :func:`json.dumps`.
"""
cls = self._app.json_encoder
bp = self._app.blueprints.get(request.blueprint) if request else None
if bp is not None and bp.json_encoder is not None:
cls = bp.json_encoder
if cls is not None:
import warnings
warnings.warn(
"Setting 'json_encoder' on the app or a blueprint is"
" deprecated and will be removed in Flask 2.3."
" Customize 'app.json' instead.",
DeprecationWarning,
)
kwargs.setdefault("cls", cls)
if "default" not in cls.__dict__:
kwargs.setdefault("default", self.default)
else:
kwargs.setdefault("default", self.default)
ensure_ascii = self._app.config["JSON_AS_ASCII"]
sort_keys = self._app.config["JSON_SORT_KEYS"]
if ensure_ascii is not None:
import warnings
warnings.warn(
"The 'JSON_AS_ASCII' config key is deprecated and will"
" be removed in Flask 2.3. Set 'app.json.ensure_ascii'"
" instead.",
DeprecationWarning,
)
else:
ensure_ascii = self.ensure_ascii
if sort_keys is not None:
import warnings
warnings.warn(
"The 'JSON_SORT_KEYS' config key is deprecated and will"
" be removed in Flask 2.3. Set 'app.json.sort_keys'"
" instead.",
DeprecationWarning,
)
else:
sort_keys = self.sort_keys
kwargs.setdefault("ensure_ascii", ensure_ascii)
kwargs.setdefault("sort_keys", sort_keys)
return json.dumps(obj, **kwargs)
def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any:
"""Deserialize data as JSON from a string or bytes.
:param s: Text or UTF-8 bytes.
:param kwargs: Passed to :func:`json.loads`.
"""
cls = self._app.json_decoder
bp = self._app.blueprints.get(request.blueprint) if request else None
if bp is not None and bp.json_decoder is not None:
cls = bp.json_decoder
if cls is not None:
import warnings
warnings.warn(
"Setting 'json_decoder' on the app or a blueprint is"
" deprecated and will be removed in Flask 2.3."
" Customize 'app.json' instead.",
DeprecationWarning,
)
kwargs.setdefault("cls", cls)
return json.loads(s, **kwargs)
def response(self, *args: t.Any, **kwargs: t.Any) -> Response:
"""Serialize the given arguments as JSON, and return a
:class:`~flask.Response` object with it. The response mimetype
will be "application/json" and can be changed with
:attr:`mimetype`.
If :attr:`compact` is ``False`` or debug mode is enabled, the
output will be formatted to be easier to read.
Either positional or keyword arguments can be given, not both.
If no arguments are given, ``None`` is serialized.
:param args: A single value to serialize, or multiple values to
treat as a list to serialize.
:param kwargs: Treat as a dict to serialize.
"""
obj = self._prepare_response_obj(args, kwargs)
dump_args: t.Dict[str, t.Any] = {}
pretty = self._app.config["JSONIFY_PRETTYPRINT_REGULAR"]
mimetype = self._app.config["JSONIFY_MIMETYPE"]
if pretty is not None:
import warnings
warnings.warn(
"The 'JSONIFY_PRETTYPRINT_REGULAR' config key is"
" deprecated and will be removed in Flask 2.3. Set"
" 'app.json.compact' instead.",
DeprecationWarning,
)
compact: bool | None = not pretty
else:
compact = self.compact
if (compact is None and self._app.debug) or compact is False:
dump_args.setdefault("indent", 2)
else:
dump_args.setdefault("separators", (",", ":"))
if mimetype is not None:
import warnings
warnings.warn(
"The 'JSONIFY_MIMETYPE' config key is deprecated and"
" will be removed in Flask 2.3. Set 'app.json.mimetype'"
" instead.",
DeprecationWarning,
)
else:
mimetype = self.mimetype
return self._app.response_class(
f"{self.dumps(obj, **dump_args)}\n", mimetype=mimetype
)

View file

@ -6,8 +6,6 @@ import sys
import typing as t
from collections import defaultdict
from functools import update_wrapper
from json import JSONDecoder
from json import JSONEncoder
from jinja2 import FileSystemLoader
from werkzeug.exceptions import default_exceptions
@ -76,11 +74,17 @@ class Scaffold:
#: JSON encoder class used by :func:`flask.json.dumps`. If a
#: blueprint sets this, it will be used instead of the app's value.
json_encoder: t.Optional[t.Type[JSONEncoder]] = None
#:
#: .. deprecated:: 2.2
#: Will be removed in Flask 2.3.
json_encoder: None = None
#: JSON decoder class used by :func:`flask.json.loads`. If a
#: blueprint sets this, it will be used instead of the app's value.
json_decoder: t.Optional[t.Type[JSONDecoder]] = None
#:
#: .. deprecated:: 2.2
#: Will be removed in Flask 2.3.
json_decoder: None = None
def __init__(
self,

View file

@ -12,7 +12,6 @@ from werkzeug.wrappers import Request as BaseRequest
from .cli import ScriptInfo
from .globals import _cv_request
from .json import dumps as json_dumps
from .sessions import SessionMixin
if t.TYPE_CHECKING: # pragma: no cover
@ -89,8 +88,7 @@ class EnvironBuilder(werkzeug.test.EnvironBuilder):
The serialization will be configured according to the config associated
with this EnvironBuilder's ``app``.
"""
kwargs.setdefault("app", self.app)
return json_dumps(obj, **kwargs)
return self.app.json.dumps(obj, **kwargs)
class FlaskClient(Client):
@ -227,6 +225,7 @@ class FlaskClient(Client):
buffered=buffered,
follow_redirects=follow_redirects,
)
response.json_module = self.application.json # type: ignore[misc]
# Re-push contexts that were preserved during the request.
while self._new_contexts:

View file

@ -1302,28 +1302,17 @@ def test_make_response_with_response_instance(app, req_ctx):
assert rv.headers["X-Foo"] == "bar"
def test_jsonify_no_prettyprint(app, req_ctx):
app.config.update({"JSONIFY_PRETTYPRINT_REGULAR": False})
compressed_msg = b'{"msg":{"submsg":"W00t"},"msg2":"foobar"}\n'
uncompressed_msg = {"msg": {"submsg": "W00t"}, "msg2": "foobar"}
rv = flask.make_response(flask.jsonify(uncompressed_msg), 200)
assert rv.data == compressed_msg
def test_jsonify_prettyprint(app, req_ctx):
app.config.update({"JSONIFY_PRETTYPRINT_REGULAR": True})
compressed_msg = {"msg": {"submsg": "W00t"}, "msg2": "foobar"}
pretty_response = (
b'{\n "msg": {\n "submsg": "W00t"\n }, \n "msg2": "foobar"\n}\n'
)
rv = flask.make_response(flask.jsonify(compressed_msg), 200)
assert rv.data == pretty_response
@pytest.mark.parametrize("compact", [True, False])
def test_jsonify_no_prettyprint(app, compact):
app.json.compact = compact
rv = app.json.response({"msg": {"submsg": "W00t"}, "msg2": "foobar"})
data = rv.data.strip()
assert (b" " not in data) is compact
assert (b"\n" not in data) is compact
def test_jsonify_mimetype(app, req_ctx):
app.config.update({"JSONIFY_MIMETYPE": "application/vnd.api+json"})
app.json.mimetype = "application/vnd.api+json"
msg = {"msg": {"submsg": "W00t"}}
rv = flask.make_response(flask.jsonify(msg), 200)
assert rv.mimetype == "application/vnd.api+json"
@ -1333,15 +1322,15 @@ def test_json_dump_dataclass(app, req_ctx):
from dataclasses import make_dataclass
Data = make_dataclass("Data", [("name", str)])
value = flask.json.dumps(Data("Flask"), app=app)
value = flask.json.loads(value, app=app)
value = app.json.dumps(Data("Flask"))
value = app.json.loads(value)
assert value == {"name": "Flask"}
def test_jsonify_args_and_kwargs_check(app, req_ctx):
with pytest.raises(TypeError) as e:
flask.jsonify("fake args", kwargs="fake")
assert "behavior undefined" in str(e.value)
assert "args or kwargs" in str(e.value)
def test_url_generation(app, req_ctx):

View file

@ -8,6 +8,7 @@ from werkzeug.http import http_date
import flask
from flask import json
from flask.json.provider import DefaultJSONProvider
@pytest.mark.parametrize("debug", (True, False))
@ -48,9 +49,8 @@ def test_json_custom_mimetypes(app, client):
"test_value,expected", [(True, '"\\u2603"'), (False, '"\u2603"')]
)
def test_json_as_unicode(test_value, expected, app, app_ctx):
app.config["JSON_AS_ASCII"] = test_value
rv = flask.json.dumps("\N{SNOWMAN}")
app.json.ensure_ascii = test_value
rv = app.json.dumps("\N{SNOWMAN}")
assert rv == expected
@ -170,7 +170,7 @@ def test_jsonify_aware_datetimes(tz):
dt = datetime.datetime(2017, 1, 1, 12, 34, 56, tzinfo=tzinfo)
gmt = FixedOffset(hours=0, name="GMT")
expected = dt.astimezone(gmt).strftime('"%a, %d %b %Y %H:%M:%S %Z"')
assert flask.json.JSONEncoder().encode(dt) == expected
assert flask.json.dumps(dt) == expected
def test_jsonify_uuid_types(app, client):
@ -225,24 +225,25 @@ def test_json_customization(app, client):
def __init__(self, val):
self.val = val
class MyEncoder(flask.json.JSONEncoder):
def default(self, o):
if isinstance(o, X):
return f"<{o.val}>"
return flask.json.JSONEncoder.default(self, o)
def default(o):
if isinstance(o, X):
return f"<{o.val}>"
class MyDecoder(flask.json.JSONDecoder):
def __init__(self, *args, **kwargs):
kwargs.setdefault("object_hook", self.object_hook)
flask.json.JSONDecoder.__init__(self, *args, **kwargs)
return DefaultJSONProvider.default(o)
class CustomProvider(DefaultJSONProvider):
def object_hook(self, obj):
if len(obj) == 1 and "_foo" in obj:
return X(obj["_foo"])
return obj
app.json_encoder = MyEncoder
app.json_decoder = MyDecoder
def loads(self, s, **kwargs):
kwargs.setdefault("object_hook", self.object_hook)
return super().loads(s, **kwargs)
app.json = CustomProvider(app)
app.json.default = default
@app.route("/", methods=["POST"])
def index():
@ -256,49 +257,6 @@ def test_json_customization(app, client):
assert rv.data == b'"<42>"'
def test_blueprint_json_customization(app, client):
class X:
__slots__ = ("val",)
def __init__(self, val):
self.val = val
class MyEncoder(flask.json.JSONEncoder):
def default(self, o):
if isinstance(o, X):
return f"<{o.val}>"
return flask.json.JSONEncoder.default(self, o)
class MyDecoder(flask.json.JSONDecoder):
def __init__(self, *args, **kwargs):
kwargs.setdefault("object_hook", self.object_hook)
flask.json.JSONDecoder.__init__(self, *args, **kwargs)
def object_hook(self, obj):
if len(obj) == 1 and "_foo" in obj:
return X(obj["_foo"])
return obj
bp = flask.Blueprint("bp", __name__)
bp.json_encoder = MyEncoder
bp.json_decoder = MyDecoder
@bp.route("/bp", methods=["POST"])
def index():
return flask.json.dumps(flask.request.get_json()["x"])
app.register_blueprint(bp)
rv = client.post(
"/bp",
data=flask.json.dumps({"x": {"_foo": 42}}),
content_type="application/json",
)
assert rv.data == b'"<42>"'
def _has_encoding(name):
try:
import codecs
@ -330,8 +288,7 @@ def test_modified_url_encoding(app, client):
def test_json_key_sorting(app, client):
app.debug = True
assert app.config["JSON_SORT_KEYS"]
assert app.json.sort_keys
d = dict.fromkeys(range(20), "foo")
@app.route("/")

View file

@ -112,7 +112,7 @@ def test_path_is_url(app):
def test_environbuilder_json_dumps(app):
"""EnvironBuilder.json_dumps() takes settings from the app."""
app.config["JSON_AS_ASCII"] = False
app.json.ensure_ascii = False
eb = EnvironBuilder(app, json="\u20ac")
assert eb.input_stream.read().decode("utf8") == '"\u20ac"'