I had to put together a small web app the other day, using SQLAlchemy and Flask. Because I hate writing code multiple times, when I can do things using a better way, I wanted to be able to serialise SQLAlchemy ORM objects straight to JSON.
I decided on an approach where taking a leaf out of Javascript, I would optionally implement a tojson()
method on a class, which I would attempt to call from my JSONEncoder
((The tojson()
method actually returns a Python dict understandable by JSONEncoder
)).
It turns out to be relatively simple to extend SQLAlchemy’s declarative base class to add additional methods (we can also use this as an excuse to implement a general __repr__()
.
from sqlalchemy.ext.declarative import declarative_base as real_declarative_base
# Let's make this a class decorator
declarative_base = lambda cls: real_declarative_base(cls=cls)
@declarative_base
class Base(object):
"""
Add some default properties and methods to the SQLAlchemy declarative base.
"""
@property
def columns(self):
return [ c.name for c in self.__table__.columns ]
@property
def columnitems(self):
return dict([ (c, getattr(self, c)) for c in self.columns ])
def __repr__(self):
return '{}({})'.format(self.__class__.__name__, self.columnitems)
def tojson(self):
return self.columnitems
We can then define our tables in the usual way:
class Client(Base):
__tablename__ = 'client'
...
You can obviously replace any of the methods in your subclass, if you don’t want to serialise the whole thing. Bonus points for anyone who wants to extend this to serialise one-to-many relationships.
And what about calling the tojson()
method? That’s easy, we can just provide our own JSONEncoder.
import json
class JSONEncoder(json.JSONEncoder):
"""
Wrapper class to try calling an object's tojson() method. This allows
us to JSONify objects coming from the ORM. Also handles dates and datetimes.
"""
def default(self, obj):
if isinstance(obj, datetime.date):
return obj.isoformat()
try:
return obj.tojson()
except AttributeError:
return json.JSONEncoder.default(self, obj)
Cutting edge Flask provides a way to replace the default JSON encoder, but the version I got out of pip does not. This is relatively easy to work around though by replacing jsonify
with our own version.
from flask import Flask
app = Flask(__name__)
def jsonify(*args, **kwargs):
"""
Workaround for Flask's jsonify not allowing replacement of the JSONEncoder
in my version of Flask.
"""
return app.response_class(json.dumps(dict(*args, **kwargs),
cls=JSONEncoder),
mimetype='application/json')
If you do have a newer Flask, where you don’t have to replace jsonify, you can also inherit from Flask’s JSONEncoder, which already handles things like datetimes for you.