1from __future__ import annotations
2
3import dataclasses
4import decimal
5import json
6import typing as t
7import uuid
8import weakref
9from datetime import date
10
11from werkzeug.http import http_date
12
13from ..globals import request
14
15if t.TYPE_CHECKING: # pragma: no cover
16 from ..app import Flask
17 from ..wrappers import Response
18
19
20class JSONProvider:
21 """A standard set of JSON operations for an application. Subclasses
22 of this can be used to customize JSON behavior or use different
23 JSON libraries.
24
25 To implement a provider for a specific library, subclass this base
26 class and implement at least :meth:`dumps` and :meth:`loads`. All
27 other methods have default implementations.
28
29 To use a different provider, either subclass ``Flask`` and set
30 :attr:`~flask.Flask.json_provider_class` to a provider class, or set
31 :attr:`app.json <flask.Flask.json>` to an instance of the class.
32
33 :param app: An application instance. This will be stored as a
34 :class:`weakref.proxy` on the :attr:`_app` attribute.
35
36 .. versionadded:: 2.2
37 """
38
39 def __init__(self, app: Flask) -> None:
40 self._app = weakref.proxy(app)
41
42 def dumps(self, obj: t.Any, **kwargs: t.Any) -> str:
43 """Serialize data as JSON.
44
45 :param obj: The data to serialize.
46 :param kwargs: May be passed to the underlying JSON library.
47 """
48 raise NotImplementedError
49
50 def dump(self, obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None:
51 """Serialize data as JSON and write to a file.
52
53 :param obj: The data to serialize.
54 :param fp: A file opened for writing text. Should use the UTF-8
55 encoding to be valid JSON.
56 :param kwargs: May be passed to the underlying JSON library.
57 """
58 fp.write(self.dumps(obj, **kwargs))
59
60 def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any:
61 """Deserialize data as JSON.
62
63 :param s: Text or UTF-8 bytes.
64 :param kwargs: May be passed to the underlying JSON library.
65 """
66 raise NotImplementedError
67
68 def load(self, fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any:
69 """Deserialize data as JSON read from a file.
70
71 :param fp: A file opened for reading text or UTF-8 bytes.
72 :param kwargs: May be passed to the underlying JSON library.
73 """
74 return self.loads(fp.read(), **kwargs)
75
76 def _prepare_response_obj(
77 self, args: t.Tuple[t.Any, ...], kwargs: t.Dict[str, t.Any]
78 ) -> t.Any:
79 if args and kwargs:
80 raise TypeError("app.json.response() takes either args or kwargs, not both")
81
82 if not args and not kwargs:
83 return None
84
85 if len(args) == 1:
86 return args[0]
87
88 return args or kwargs
89
90 def response(self, *args: t.Any, **kwargs: t.Any) -> Response:
91 """Serialize the given arguments as JSON, and return a
92 :class:`~flask.Response` object with the ``application/json``
93 mimetype.
94
95 The :func:`~flask.json.jsonify` function calls this method for
96 the current application.
97
98 Either positional or keyword arguments can be given, not both.
99 If no arguments are given, ``None`` is serialized.
100
101 :param args: A single value to serialize, or multiple values to
102 treat as a list to serialize.
103 :param kwargs: Treat as a dict to serialize.
104 """
105 obj = self._prepare_response_obj(args, kwargs)
106 return self._app.response_class(self.dumps(obj), mimetype="application/json")
107
108
109def _default(o: t.Any) -> t.Any:
110 if isinstance(o, date):
111 return http_date(o)
112
113 if isinstance(o, (decimal.Decimal, uuid.UUID)):
114 return str(o)
115
116 if dataclasses and dataclasses.is_dataclass(o):
117 return dataclasses.asdict(o)
118
119 if hasattr(o, "__html__"):
120 return str(o.__html__())
121
122 raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
123
124
125class DefaultJSONProvider(JSONProvider):
126 """Provide JSON operations using Python's built-in :mod:`json`
127 library. Serializes the following additional data types:
128
129 - :class:`datetime.datetime` and :class:`datetime.date` are
130 serialized to :rfc:`822` strings. This is the same as the HTTP
131 date format.
132 - :class:`uuid.UUID` is serialized to a string.
133 - :class:`dataclasses.dataclass` is passed to
134 :func:`dataclasses.asdict`.
135 - :class:`~markupsafe.Markup` (or any object with a ``__html__``
136 method) will call the ``__html__`` method to get a string.
137 """
138
139 default: t.Callable[[t.Any], t.Any] = staticmethod(
140 _default
141 ) # type: ignore[assignment]
142 """Apply this function to any object that :meth:`json.dumps` does
143 not know how to serialize. It should return a valid JSON type or
144 raise a ``TypeError``.
145 """
146
147 ensure_ascii = True
148 """Replace non-ASCII characters with escape sequences. This may be
149 more compatible with some clients, but can be disabled for better
150 performance and size.
151 """
152
153 sort_keys = True
154 """Sort the keys in any serialized dicts. This may be useful for
155 some caching situations, but can be disabled for better performance.
156 When enabled, keys must all be strings, they are not converted
157 before sorting.
158 """
159
160 compact: bool | None = None
161 """If ``True``, or ``None`` out of debug mode, the :meth:`response`
162 output will not add indentation, newlines, or spaces. If ``False``,
163 or ``None`` in debug mode, it will use a non-compact representation.
164 """
165
166 mimetype = "application/json"
167 """The mimetype set in :meth:`response`."""
168
169 def dumps(self, obj: t.Any, **kwargs: t.Any) -> str:
170 """Serialize data as JSON to a string.
171
172 Keyword arguments are passed to :func:`json.dumps`. Sets some
173 parameter defaults from the :attr:`default`,
174 :attr:`ensure_ascii`, and :attr:`sort_keys` attributes.
175
176 :param obj: The data to serialize.
177 :param kwargs: Passed to :func:`json.dumps`.
178 """
179 cls = self._app._json_encoder
180 bp = self._app.blueprints.get(request.blueprint) if request else None
181
182 if bp is not None and bp._json_encoder is not None:
183 cls = bp._json_encoder
184
185 if cls is not None:
186 import warnings
187
188 warnings.warn(
189 "Setting 'json_encoder' on the app or a blueprint is"
190 " deprecated and will be removed in Flask 2.3."
191 " Customize 'app.json' instead.",
192 DeprecationWarning,
193 )
194 kwargs.setdefault("cls", cls)
195
196 if "default" not in cls.__dict__:
197 kwargs.setdefault("default", self.default)
198 else:
199 kwargs.setdefault("default", self.default)
200
201 ensure_ascii = self._app.config["JSON_AS_ASCII"]
202 sort_keys = self._app.config["JSON_SORT_KEYS"]
203
204 if ensure_ascii is not None:
205 import warnings
206
207 warnings.warn(
208 "The 'JSON_AS_ASCII' config key is deprecated and will"
209 " be removed in Flask 2.3. Set 'app.json.ensure_ascii'"
210 " instead.",
211 DeprecationWarning,
212 )
213 else:
214 ensure_ascii = self.ensure_ascii
215
216 if sort_keys is not None:
217 import warnings
218
219 warnings.warn(
220 "The 'JSON_SORT_KEYS' config key is deprecated and will"
221 " be removed in Flask 2.3. Set 'app.json.sort_keys'"
222 " instead.",
223 DeprecationWarning,
224 )
225 else:
226 sort_keys = self.sort_keys
227
228 kwargs.setdefault("ensure_ascii", ensure_ascii)
229 kwargs.setdefault("sort_keys", sort_keys)
230 return json.dumps(obj, **kwargs)
231
232 def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any:
233 """Deserialize data as JSON from a string or bytes.
234
235 :param s: Text or UTF-8 bytes.
236 :param kwargs: Passed to :func:`json.loads`.
237 """
238 cls = self._app._json_decoder
239 bp = self._app.blueprints.get(request.blueprint) if request else None
240
241 if bp is not None and bp._json_decoder is not None:
242 cls = bp._json_decoder
243
244 if cls is not None:
245 import warnings
246
247 warnings.warn(
248 "Setting 'json_decoder' on the app or a blueprint is"
249 " deprecated and will be removed in Flask 2.3."
250 " Customize 'app.json' instead.",
251 DeprecationWarning,
252 )
253 kwargs.setdefault("cls", cls)
254
255 return json.loads(s, **kwargs)
256
257 def response(self, *args: t.Any, **kwargs: t.Any) -> Response:
258 """Serialize the given arguments as JSON, and return a
259 :class:`~flask.Response` object with it. The response mimetype
260 will be "application/json" and can be changed with
261 :attr:`mimetype`.
262
263 If :attr:`compact` is ``False`` or debug mode is enabled, the
264 output will be formatted to be easier to read.
265
266 Either positional or keyword arguments can be given, not both.
267 If no arguments are given, ``None`` is serialized.
268
269 :param args: A single value to serialize, or multiple values to
270 treat as a list to serialize.
271 :param kwargs: Treat as a dict to serialize.
272 """
273 obj = self._prepare_response_obj(args, kwargs)
274 dump_args: t.Dict[str, t.Any] = {}
275 pretty = self._app.config["JSONIFY_PRETTYPRINT_REGULAR"]
276 mimetype = self._app.config["JSONIFY_MIMETYPE"]
277
278 if pretty is not None:
279 import warnings
280
281 warnings.warn(
282 "The 'JSONIFY_PRETTYPRINT_REGULAR' config key is"
283 " deprecated and will be removed in Flask 2.3. Set"
284 " 'app.json.compact' instead.",
285 DeprecationWarning,
286 )
287 compact: bool | None = not pretty
288 else:
289 compact = self.compact
290
291 if (compact is None and self._app.debug) or compact is False:
292 dump_args.setdefault("indent", 2)
293 else:
294 dump_args.setdefault("separators", (",", ":"))
295
296 if mimetype is not None:
297 import warnings
298
299 warnings.warn(
300 "The 'JSONIFY_MIMETYPE' config key is deprecated and"
301 " will be removed in Flask 2.3. Set 'app.json.mimetype'"
302 " instead.",
303 DeprecationWarning,
304 )
305 else:
306 mimetype = self.mimetype
307
308 return self._app.response_class(
309 f"{self.dumps(obj, **dump_args)}\n", mimetype=mimetype
310 )