miLibris / flask-rest-jsonapi

Flask extension to build REST APIs around JSONAPI 1.0 specification.
http://flask-rest-jsonapi.readthedocs.io
MIT License
598 stars 153 forks source link

Serialization of sqlalchemy proxied dictionary based collections fails #180

Open ricardog opened 4 years ago

ricardog commented 4 years ago

Hi --

My model uses a sqlalchemy dictionary based collection proxy. When serializing a model instance I get the following exception

  File "/usr/lib/python3.7/site-packages/flask/app.py", line 2463, in __call__
    return self.wsgi_app(environ, start_response)
  File "/usr/lib/python3.7/site-packages/werkzeug/middleware/dispatcher.py", line 66, in __call__
    return app(environ, start_response)
  File "/usr/lib/python3.7/site-packages/flask/app.py", line 2449, in wsgi_app
    response = self.handle_exception(e)
  File "/usr/lib/python3.7/site-packages/flask/app.py", line 1866, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/lib/python3.7/site-packages/flask/_compat.py", line 39, in reraise
    raise value
  File "/usr/lib/python3.7/site-packages/flask/app.py", line 2446, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/lib/python3.7/site-packages/flask/app.py", line 1951, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/lib/python3.7/site-packages/flask/app.py", line 1820, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/lib/python3.7/site-packages/flask/_compat.py", line 39, in reraise
    raise value
  File "/usr/lib/python3.7/site-packages/flask/app.py", line 1949, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/lib/python3.7/site-packages/flask/app.py", line 1935, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/usr/lib/python3.7/site-packages/flask_rest_jsonapi/decorators.py", line 47, in wrapper
    return func(*args, **kwargs)
  File "/usr/lib/python3.7/site-packages/flask/views.py", line 89, in view
    return self.dispatch_request(*args, **kwargs)
  File "/usr/lib/python3.7/site-packages/flask_rest_jsonapi/decorators.py", line 83, in wrapper
    raise e
  File "/usr/lib/python3.7/site-packages/flask_rest_jsonapi/decorators.py", line 76, in wrapper
    return func(*args, **kwargs)
  File "/usr/lib/python3.7/site-packages/flask_rest_jsonapi/resource.py", line 81, in dispatch_request
    return make_response(json.dumps(response, cls=JSONEncoder), 200, headers)
  File "/usr/lib/python3.7/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "/usr/lib/python3.7/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib/python3.7/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "/usr/lib/python3.7/site-packages/flask_rest_jsonapi/utils.py", line 26, in default
    return json.JSONEncoder.default(self, obj)
  File "/usr/lib/python3.7/json/encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type _AssociationDict is not JSON serializable

The sqlalchemy proxy object is implemented using the _AssociationDict class which the JSON encoder does not know how to serialize. The marshmallow schema for the object is defined as

class ResourceSchema(Schema):
    id = fields.Integer(as_string=True, dump_only=True)
    title = fields.Str()
    url = fields.Str()
    language = fields.Str()
    tags = fields.Dict(keys=fields.Str(), values=fields.Str())

I didn't see a way to pass in a specialized JSON encoder to solve this. So I changed utils.py as follows (for reference see the flask-restless implementation)

import json
from uuid import UUID
from datetime import datetime

from sqlalchemy.ext.associationproxy import _AssociationDict
from sqlalchemy.ext.associationproxy import _AssociationList
from sqlalchemy.ext.associationproxy import _AssociationSet

class JSONEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.isoformat()
        # Attributes values that come from association proxy
        # collections need to be cast to plain old Python data types
        # so that the JSON serializer can handle them.
        if isinstance(obj, _AssociationList):
            return list(obj)
        if isinstance(obj, _AssociationSet):
            return set(obj)
        if isinstance(obj, _AssociationDict):
            return dict(obj)
        if isinstance(obj, UUID):
            return str(obj)
        return json.JSONEncoder.default(self, obj)

Alternatively, allowing the user to pass a class for the JSON encoder would also work.