Skip to content

add JSON provider interface #4692

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 13, 2022
Merged

add JSON provider interface #4692

merged 1 commit into from
Jul 13, 2022

Conversation

davidism
Copy link
Member

@davidism davidism commented Jul 13, 2022

This adds the ability to fully customize the JSON implementation used by a Flask application. Using a different JSON implementation can greatly speed up API applications that need to work with JSON in most requests.

app.json is an instance of Flask.json_provider_class. flask.json.provider.JSONProvider is the base class that defines dumps, dump, loads, load, and response methods, of which only dumps and loads need to be implemented. For example, here's a provider for orjson:

from flask.json.provider import JSONProvider
import orjson

class OrJSONProvider(JSONProvider):
    def dumps(self, obj, *, option=None, **kwargs):
        if option is None:
            option = orjson.OPT_APPEND_NEWLINE | orjson.OPT_NAIVE_UTC
        
        return orjson.dumps(obj, option=option).decode()

    def loads(self, s, **kwargs):
        return orjson.loads(s)

# assign to an app instance
app.json = OrJSONProvider(app)

# or assign in a subclass
class MyFlask(Flask):
    json_provider_class = OrJSONProvider

app = MyFlask(__name__)

The methods in flask.json call the methods on app.json if an app context is active, or fall back to the json library. jsonify calls app.json.response. The |tojson filter uses app.json.dumps. Request.json uses app.json.loads and Response.json uses app.json.dumps; the test client uses these as well.

Customizing json_encoder or json_decoder on an app or blueprint, and the JSONEncoder and JSONDecoder classes, are deprecated. This was not an effective way to use other libraries. Customizing per blueprint was requested by an API extension that is no longer maintained and didn't appear to use the feature. It's not clear how it would work with the new provider interface and added overhead to every request. Instead, API frameworks should be using a dedicated object serialization library, then taking advantage of a fast JSON serializer at the application level.

The DefaultJSONProvider is the existing implementation using the built-in json library. The app.config keys JSON_AS_ASCII, JSON_SORT_KEYS, JSONIFY_MIMETYPE, and JSONIFY_PRETTYPRINT_REGULAR are deprecated and have moved to attributes on the default provider. Other providers are not required to support these options.

@davidism davidism added this to the 2.2.0 milestone Jul 13, 2022
@davidism davidism added the json label Jul 13, 2022
@davidism davidism merged commit 67310ab into main Jul 13, 2022
@davidism davidism deleted the json-provider branch July 13, 2022 16:55
: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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth a cls default_mimetype here to save extensions having to override this method and as matching with responses?

Copy link
Member Author

@davidism davidism Jul 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed this more here: #1728 (comment)

I'm not sure this should actually be configurable at all. The original issue seemed to be about a specific type of response, not all JSON responses. Maybe if you want your whole API to have a different vendor type, like GitHub does, but even GitHub applies different mimetypes to different parts. APIs complex enough to use vendor types usually have versioning as well, so they still wouldn't apply globally.

I did originally have this as a JSONProvider.mimetype attribute, but I ended up moving all existing behavior to DefaultProvider and keeping the base very simple.

This comment was marked as off-topic.

This comment was marked as off-topic.

This comment was marked as off-topic.

not know how to serialize. It should return a valid JSON type or
raise a ``TypeError``.
"""

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about adding a dict_to_object hook here as well (for the loads side). Allows something like this,

class MoneyJSONProvider(DefaultJSONProvider):

        @staticmethod
        def default(object_):
            if isinstance(object_, date):
                return http_date(object_)
            if isinstance(object_, (Decimal, UUID)):
                return str(object_)
            if is_dataclass(object_):
                return asdict(object_)
            if hasattr(object_, "__html__"):
                return str(object_.__html__())
            if isinstance(object_, Money):
                return {'amount': object_.amount, 'currency': object_.currency}

            raise TypeError(f"Object of type {type(object_).__name__} is not JSON serializable")

        @staticmethod
        def dict_to_object(dict_):
            if 'amount' in dict_ and 'currency' in dict_:
                return Money(Decimal(dict_['amount']), dict_['currency'])
            else:
                return dict_ 

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left it out for a few reasons. object_hook is not as consistently supported by different libraries as default is, and I didn't want to put a perceived requirement for it on all other providers. You usually want to perform validation when deserializing, and that gets very messy trying to cram it all in object_hook along with proper error collection. Instead, any project should use a serialization library, leaving the provider to only handle the JSON and basic types.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 2, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants