# -*- coding: utf-8 -*- """ flask.json ~~~~~~~~~~ :copyright: 2010 Pallets :license: BSD-3-Clause """ import codecs import io import uuid from datetime import date from datetime import datetime from itsdangerous import json as _json from jinja2 import Markup from werkzeug.http import http_date from .._compat import PY2 from .._compat import text_type from ..globals import current_app from ..globals import request try: import dataclasses except ImportError: dataclasses = None # Figure out if simplejson escapes slashes. This behavior was changed # from one version to another without reason. _slash_escape = "\\/" not in _json.dumps("/") __all__ = [ "dump", "dumps", "load", "loads", "htmlsafe_dump", "htmlsafe_dumps", "JSONDecoder", "JSONEncoder", "jsonify", ] def _wrap_reader_for_text(fp, encoding): if isinstance(fp.read(0), bytes): fp = io.TextIOWrapper(io.BufferedReader(fp), encoding) return fp def _wrap_writer_for_text(fp, encoding): try: fp.write("") except TypeError: fp = io.TextIOWrapper(fp, encoding) return fp class JSONEncoder(_json.JSONEncoder): """The default Flask JSON encoder. This one extends the default encoder by also supporting ``datetime``, ``UUID``, ``dataclasses``, and ``Markup`` objects. ``datetime`` objects are serialized as RFC 822 datetime strings. This is the same as the HTTP date format. In order to support more data types, override the :meth:`default` method. """ def default(self, o): """Implement this method in a subclass such that it returns a serializable object for ``o``, or calls the base implementation (to raise a :exc:`TypeError`). For example, to support arbitrary iterators, you could implement default like this:: def default(self, o): try: iterable = iter(o) except TypeError: pass else: return list(iterable) return JSONEncoder.default(self, o) """ if isinstance(o, datetime): return http_date(o.utctimetuple()) if isinstance(o, date): return http_date(o.timetuple()) if isinstance(o, uuid.UUID): return str(o) if dataclasses and dataclasses.is_dataclass(o): return dataclasses.asdict(o) if hasattr(o, "__html__"): return text_type(o.__html__()) return _json.JSONEncoder.default(self, o) class JSONDecoder(_json.JSONDecoder): """The default JSON decoder. This one does not change the behavior from the default simplejson decoder. Consult the :mod:`json` documentation for more information. This decoder is not only used for the load functions of this module but also :attr:`~flask.Request`. """ def _dump_arg_defaults(kwargs, app=None): """Inject default arguments for dump functions.""" if app is None: app = current_app if app: bp = app.blueprints.get(request.blueprint) if request else None kwargs.setdefault( "cls", bp.json_encoder if bp and bp.json_encoder else app.json_encoder ) if not app.config["JSON_AS_ASCII"]: kwargs.setdefault("ensure_ascii", False) kwargs.setdefault("sort_keys", app.config["JSON_SORT_KEYS"]) else: kwargs.setdefault("sort_keys", True) kwargs.setdefault("cls", JSONEncoder) def _load_arg_defaults(kwargs, app=None): """Inject default arguments for load functions.""" if app is None: app = current_app if app: bp = app.blueprints.get(request.blueprint) if request else None kwargs.setdefault( "cls", bp.json_decoder if bp and bp.json_decoder else app.json_decoder ) else: kwargs.setdefault("cls", JSONDecoder) def detect_encoding(data): """Detect which UTF codec was used to encode the given bytes. The latest JSON standard (:rfc:`8259`) suggests that only UTF-8 is accepted. Older documents allowed 8, 16, or 32. 16 and 32 can be big or little endian. Some editors or libraries may prepend a BOM. :param data: Bytes in unknown UTF encoding. :return: UTF encoding name """ head = data[:4] if head[:3] == codecs.BOM_UTF8: return "utf-8-sig" if b"\x00" not in head: return "utf-8" if head in (codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE): return "utf-32" if head[:2] in (codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE): return "utf-16" if len(head) == 4: if head[:3] == b"\x00\x00\x00": return "utf-32-be" if head[::2] == b"\x00\x00": return "utf-16-be" if head[1:] == b"\x00\x00\x00": return "utf-32-le" if head[1::2] == b"\x00\x00": return "utf-16-le" if len(head) == 2: return "utf-16-be" if head.startswith(b"\x00") else "utf-16-le" return "utf-8" def dumps(obj, app=None, **kwargs): """Serialize ``obj`` to a JSON-formatted string. If there is an app context pushed, use the current app's configured encoder (:attr:`~flask.Flask.json_encoder`), or fall back to the default :class:`JSONEncoder`. Takes the same arguments as the built-in :func:`json.dumps`, and does some extra configuration based on the application. If the simplejson package is installed, it is preferred. :param obj: Object to serialize to JSON. :param app: App instance to use to configure the JSON encoder. Uses ``current_app`` if not given, and falls back to the default encoder when not in an app context. :param kwargs: Extra arguments passed to :func:`json.dumps`. .. versionchanged:: 1.0.3 ``app`` can be passed directly, rather than requiring an app context for configuration. """ _dump_arg_defaults(kwargs, app=app) encoding = kwargs.pop("encoding", None) rv = _json.dumps(obj, **kwargs) if encoding is not None and isinstance(rv, text_type): rv = rv.encode(encoding) return rv def dump(obj, fp, app=None, **kwargs): """Like :func:`dumps` but writes into a file object.""" _dump_arg_defaults(kwargs, app=app) encoding = kwargs.pop("encoding", None) if encoding is not None: fp = _wrap_writer_for_text(fp, encoding) _json.dump(obj, fp, **kwargs) def loads(s, app=None, **kwargs): """Deserialize an object from a JSON-formatted string ``s``. If there is an app context pushed, use the current app's configured decoder (:attr:`~flask.Flask.json_decoder`), or fall back to the default :class:`JSONDecoder`. Takes the same arguments as the built-in :func:`json.loads`, and does some extra configuration based on the application. If the simplejson package is installed, it is preferred. :param s: JSON string to deserialize. :param app: App instance to use to configure the JSON decoder. Uses ``current_app`` if not given, and falls back to the default encoder when not in an app context. :param kwargs: Extra arguments passed to :func:`json.dumps`. .. versionchanged:: 1.0.3 ``app`` can be passed directly, rather than requiring an app context for configuration. """ _load_arg_defaults(kwargs, app=app) if isinstance(s, bytes): encoding = kwargs.pop("encoding", None) if encoding is None: encoding = detect_encoding(s) s = s.decode(encoding) return _json.loads(s, **kwargs) def load(fp, app=None, **kwargs): """Like :func:`loads` but reads from a file object.""" _load_arg_defaults(kwargs, app=app) if not PY2: fp = _wrap_reader_for_text(fp, kwargs.pop("encoding", None) or "utf-8") return _json.load(fp, **kwargs) def htmlsafe_dumps(obj, **kwargs): """Works exactly like :func:`dumps` but is safe for use in ``