1

I'm trying to create an association object UserRelationship which defines a 'follow' relationship between User and User. When I interface with this object through shell, it behaves as I expect, but when implemented onto a route function it produces a `TypeError: Object of type UserRelationship is not JSON serializable'

class UserRelationship(db.Model):
    follows_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
    bank = db.Column(db.String(100))
    follows = db.relationship('User', backref=db.backref('followers'))

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), index=True, unique=True)
    follows = db.relationship('UserRelationship', backref=db.backref('followers', lazy=True))

This produces expected results in the shell:

user1=User(username='joe')
user2=User(username='paul')
user3=User(username='phil')

new_follow = UserRelationship(follows=user2, bank='1000')
new_follow2 = UserRelationship(follows=user3, bank='1000') 

user2.follows.append(new_follow)
user2.follows.append(new_follow2)
db.session.commit()

user2.follows
>>[<UserRelationship (transient 2627038264968)>, <UserRelationship (transient 2627038286344)>]


user2.follows[0].bank
'1000'

user2.follows[0].follows
<User Paul>

However, when I have this actually on my Flask app, it runs an error:

`TypeError: Object of type UserRelationship is not JSON serializable'

Here's how I'm trying to implement on the actual application:

@app.route('/follow/<to_follow>', methods=['GET', 'POST'])
@login_required
def follow(to_follow):
    follow_target = User.query.filter_by(username=to_follow).first()
    form = FollowForm()
    if form.validate_on_submit():
        new_follow = UserRelationship(bank='1000', follows=follow_target)
        current_user.follows.append(new_follow)
        db.session.commit()
        flash(current_user.follows)
        return redirect(profile)

    return render_template('follow.html',
                           title=f'Follow {to_follow}',
                           form=form,
                           to_follow=follow_target)

Full traceback:

Traceback (most recent call last):
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\app.py", line 2446, in wsgi_app
    response = self.full_dispatch_request()
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\app.py", line 1952, in full_dispatch_request
    return self.finalize_request(rv)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\app.py", line 1969, in finalize_request
    response = self.process_response(response)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\app.py", line 2268, in process_response
    self.session_interface.save_session(self, ctx.session, response)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\sessions.py", line 378, in save_session
    val = self.get_signing_serializer(app).dumps(dict(session))
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\itsdangerous\serializer.py", line 166, in dumps
    payload = want_bytes(self.dump_payload(obj))
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\itsdangerous\url_safe.py", line 42, in dump_payload
    json = super(URLSafeSerializerMixin, self).dump_payload(obj)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\itsdangerous\serializer.py", line 133, in dump_payload
    return want_bytes(self.serializer.dumps(obj, **self.serializer_kwargs))
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\json\tag.py", line 305, in dumps
    return dumps(self.tag(value), separators=(",", ":"))
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\json\__init__.py", line 211, in dumps
    rv = _json.dumps(obj, **kwargs)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\json\__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\json\encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\json\encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\json\__init__.py", line 100, in default
    return _json.JSONEncoder.default(self, o)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\json\encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type UserRelationship is not JSON serializable
[2019-09-29 16:10:38,924] ERROR in app: Request finalizing failed with an error while handling an error
Traceback (most recent call last):
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\app.py", line 2446, in wsgi_app
    response = self.full_dispatch_request()
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\app.py", line 1952, in full_dispatch_request
    return self.finalize_request(rv)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\app.py", line 1969, in finalize_request
    response = self.process_response(response)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\app.py", line 2268, in process_response
    self.session_interface.save_session(self, ctx.session, response)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\sessions.py", line 378, in save_session
    val = self.get_signing_serializer(app).dumps(dict(session))
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\itsdangerous\serializer.py", line 166, in dumps
    payload = want_bytes(self.dump_payload(obj))
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\itsdangerous\url_safe.py", line 42, in dump_payload
    json = super(URLSafeSerializerMixin, self).dump_payload(obj)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\itsdangerous\serializer.py", line 133, in dump_payload
    return want_bytes(self.serializer.dumps(obj, **self.serializer_kwargs))
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\json\tag.py", line 305, in dumps
    return dumps(self.tag(value), separators=(",", ":"))
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\json\__init__.py", line 211, in dumps
    rv = _json.dumps(obj, **kwargs)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\json\__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\json\encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\json\encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\json\__init__.py", line 100, in default
    return _json.JSONEncoder.default(self, o)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\json\encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type UserRelationship is not JSON serializable

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\app.py", line 1969, in finalize_request
    response = self.process_response(response)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\app.py", line 2268, in process_response
    self.session_interface.save_session(self, ctx.session, response)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\sessions.py", line 378, in save_session
    val = self.get_signing_serializer(app).dumps(dict(session))
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\itsdangerous\serializer.py", line 166, in dumps
    payload = want_bytes(self.dump_payload(obj))
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\itsdangerous\url_safe.py", line 42, in dump_payload
    json = super(URLSafeSerializerMixin, self).dump_payload(obj)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\itsdangerous\serializer.py", line 133, in dump_payload
    return want_bytes(self.serializer.dumps(obj, **self.serializer_kwargs))
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\json\tag.py", line 305, in dumps
    return dumps(self.tag(value), separators=(",", ":"))
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\json\__init__.py", line 211, in dumps
    rv = _json.dumps(obj, **kwargs)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\json\__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\json\encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\json\encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\site-packages\flask\json\__init__.py", line 100, in default
    return _json.JSONEncoder.default(self, o)
  File "c:\users\phill\anaconda3\envs\zigenv\lib\json\encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type UserRelationship is not JSON serializable

phil0s0pher
  • 231
  • 1
  • 4
  • 16

1 Answers1

0

Actually, the db.Model class doesn't provides a method to serialize its instance in JSON format. For, instance, the following code, extracted from the Flask-SqlAlchemy quick start fails for the same reason.

import json

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)


class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)


if __name__ == '__main__':
    db.create_all()
    admin = User(username='admin', email='admin@example.com')

    json.dumps(admin)

You get:

Traceback (most recent call last):
  [...]
TypeError: Object of type User is not JSON serializable

So, your UserRelationship class, which inherits db.Model, is not JSON serializable.

The problem occurs when the server wants to save the user state in the current session. It dumps the User instance and then its UserRelationship.

There must be a way to avoid storing the relationships in the session. But that needs more investigation.

EDIT

IMO, the UserRelationship should be a Association Object between a User and it-self. It's a kind of self referential Many-to-many relationship with extra column.

Based on this this answer, you should define it like that:

class UserRelationship(db.Model):
    fk_user_from = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True)
    fk_user_to = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True)
    bank = db.Column(db.String(100))


class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    followings = db.relationship(
        "UserRelationship", backref="followed", primaryjoin=id == UserRelationship.fk_user_to
    )
    followers = db.relationship(
        "UserRelationship", backref="following", primaryjoin=id == UserRelationship.fk_user_from
    )

Could you retry with this schema?

Laurent LAPORTE
  • 21,958
  • 6
  • 58
  • 103