J'essaie de jsonifier un jeu de résultats SQLAlchemy dans Flask/Python.
La liste de diffusion Flask suggérait la méthode suivante http://librelist.com/browser//flask/2011/2/16/jsonify-sqlalchemy-pagination-collection-result/ # 04a0754b63387f87e59dda564bde426e :
return jsonify(json_list = qryresult)
Cependant, je reçois l'erreur suivante:
TypeError: <flaskext.sqlalchemy.BaseQuery object at 0x102c2df90>
is not JSON serializable
Qu'est-ce que je regarde ici?
J'ai trouvé cette question: Comment sérialiser le résultat de SqlAlchemy en JSON? qui semble très similaire mais je ne savais pas si Flask avait un peu de magie pour le rendre plus facile le mailing list post suggéré.
Edit: pour plus de clarté, voici à quoi ressemble mon modèle
class Rating(db.Model):
__table= 'rating'
id = db.Column(db.Integer, primary_key=True)
fullurl = db.Column(db.String())
url = db.Column(db.String())
comments = db.Column(db.Text)
overall = db.Column(db.Integer)
shipping = db.Column(db.Integer)
cost = db.Column(db.Integer)
honesty = db.Column(db.Integer)
communication = db.Column(db.Integer)
name = db.Column(db.String())
ipaddr = db.Column(db.String())
date = db.Column(db.String())
def __init__(self, fullurl, url, comments, overall, shipping, cost, honesty, communication, name, ipaddr, date):
self.fullurl = fullurl
self.url = url
self.comments = comments
self.overall = overall
self.shipping = shipping
self.cost = cost
self.honesty = honesty
self.communication = communication
self.name = name
self.ipaddr = ipaddr
self.date = date
Il semble que vous n’ayez pas exécuté votre requête. Essayez de suivre:
return jsonify(json_list = qryresult.all())
[Edit] : Le problème avec jsonify est qu’en général les objets ne peuvent pas être jsonifiés automatiquement. Même la date et l'heure de Python échouent;)
Ce que j’ai fait par le passé, c’est d’ajouter une propriété supplémentaire (telle que serialize
) aux classes devant être sérialisées.
def dump_datetime(value):
"""Deserialize datetime object into string form for JSON processing."""
if value is None:
return None
return [value.strftime("%Y-%m-%d"), value.strftime("%H:%M:%S")]
class Foo(db.Model):
# ... SQLAlchemy defs here..
def __init__(self, ...):
# self.foo = ...
pass
@property
def serialize(self):
"""Return object data in easily serializable format"""
return {
'id' : self.id,
'modified_at': dump_datetime(self.modified_at),
# This is an example how to deal with Many2Many relations
'many2many' : self.serialize_many2many
}
@property
def serialize_many2many(self):
"""
Return object's relations in easily serializable format.
NB! Calls many2many's serialize property.
"""
return [ item.serialize for item in self.many2many]
Et maintenant, pour les vues que je peux faire:
return jsonify(json_list=[i.serialize for i in qryresult.all()])
J'espère que cela t'aides ;)
[Edit 2019] : Si vous avez des objets plus complexes ou des références circulaires, utilisez une bibliothèque du type Marshmallow ).
J'ai eu le même besoin de sérialiser json. Jetez un oeil à cette question . Il montre comment découvrir les colonnes par programmation. Donc, à partir de cela, j'ai créé le code ci-dessous. Cela fonctionne pour moi et je vais l'utiliser dans mon application Web. Bonne codage!
def to_json(inst, cls):
"""
Jsonify the sql alchemy query result.
"""
convert = dict()
# add your coversions for things like datetime's
# and what-not that aren't serializable.
d = dict()
for c in cls.__table__.columns:
v = getattr(inst, c.name)
if c.type in convert.keys() and v is not None:
try:
d[c.name] = convert[c.type](v)
except:
d[c.name] = "Error: Failed to covert using ", str(convert[c.type])
Elif v is None:
d[c.name] = str()
else:
d[c.name] = v
return json.dumps(d)
class Person(base):
__table= 'person'
id = Column(Integer, Sequence('person_id_seq'), primary_key=True)
first_name = Column(Text)
last_name = Column(Text)
email = Column(Text)
@property
def json(self):
return to_json(self, self.__class__)
Voici ce qui est généralement suffisant pour moi:
Je crée un mix de sérialisation que j'utilise avec mes modèles. La fonction de sérialisation récupère les attributs exposés par l'inspecteur SQLAlchemy et les met dans un dict.
from sqlalchemy.inspection import inspect
class Serializer(object):
def serialize(self):
return {c: getattr(self, c) for c in inspect(self).attrs.keys()}
@staticmethod
def serialize_list(l):
return [m.serialize() for m in l]
Il ne reste plus qu'à étendre le modèle SQLAlchemy avec la classe Serializer
mixin.
S'il existe des champs que vous ne souhaitez pas exposer ou qui nécessitent une mise en forme spéciale, remplacez simplement la fonction serialize()
dans la sous-classe du modèle.
class User(db.Model, Serializer):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String)
password = db.Column(db.String)
# ...
def serialize(self):
d = Serializer.serialize(self)
del d['password']
return d
Dans vos contrôleurs, tout ce que vous avez à faire est d’appeler la fonction serialize()
(ou serialize_list(l)
si la requête aboutit à une liste) des résultats:
def get_user(id):
user = User.query.get(id)
return json.dumps(user.serialize())
def get_users():
users = User.query.all()
return json.dumps(User.serialize_list(users))
Voici mon approche: https://github.com/n0nSmoker/SQLAlchemy-serializer
pip installer SQLAlchemy-serializer
Vous pouvez facilement ajouter un mixin à votre modèle et simplement appeler la méthode .to_dict () sur son instance.
Vous pouvez également écrire votre propre mixin sur la base de SerializerMixin
Ok, je travaille dessus depuis quelques heures et j'ai développé ce que je pense être la solution la plus pythonique à ce jour. Les extraits de code suivants sont en python3 mais ne devraient pas être trop pénibles pour effectuer un backport si vous en avez besoin.
La première chose que nous allons faire est de commencer avec un mixage qui donne à vos modèles de base de données un peu le même effet que dict
s:
from sqlalchemy.inspection import inspect
class ModelMixin:
"""Provide dict-like interface to db.Model subclasses."""
def __getitem__(self, key):
"""Expose object attributes like dict values."""
return getattr(self, key)
def keys(self):
"""Identify what db columns we have."""
return inspect(self).attrs.keys()
Nous allons maintenant définir notre modèle, en héritant du mixin:
class MyModel(db.Model, ModelMixin):
id = db.Column(db.Integer, primary_key=True)
foo = db.Column(...)
bar = db.Column(...)
# etc ...
C’est tout ce qui est nécessaire pour pouvoir passer une instance de MyModel()
à dict()
et obtenir une instance réelle en direct dict
, ce qui nous fait parcourir un long chemin. pour que jsonify()
le comprenne. Ensuite, nous devons étendre JSONEncoder
pour obtenir le reste du chemin:
from flask.json import JSONEncoder
from contextlib import suppress
class MyJSONEncoder(JSONEncoder):
def default(self, obj):
# Optional: convert datetime objects to ISO format
with suppress(AttributeError):
return obj.isoformat()
return dict(obj)
app.json_encoder = MyJSONEncoder
Points bonus: si votre modèle contient des champs calculés (vous souhaitez que votre sortie JSON contienne des champs qui ne sont pas réellement stockés dans la base de données), rien de plus simple. Définissez simplement vos champs calculés comme @property
, Et étendez la méthode keys()
comme suit:
class MyModel(db.Model, ModelMixin):
id = db.Column(db.Integer, primary_key=True)
foo = db.Column(...)
bar = db.Column(...)
@property
def computed_field(self):
return 'this value did not come from the db'
def keys(self):
return super().keys() + ['computed_field']
Maintenant, il est trivial de jsonifier:
@app.route('/whatever', methods=['GET'])
def whatever():
return jsonify(dict(results=MyModel.query.all()))
Pour une requête simple (pas de jointure), vous pouvez le faire.
@app.route('/results/')
def results():
data = Table.query.all()
result = [d.__dict__ for d in data]
return jsonify(result=result)
et si vous souhaitez uniquement renvoyer certaines colonnes de la base de données, vous pouvez le faire.
@app.route('/results/')
def results():
cols = ['id', 'url', 'shipping']
data = Table.query.all()
result = [{col: getattr(d, col) for col in cols} for d in data]
return jsonify(result=result)
Si vous utilisez flask-restful
vous pouvez utiliser marshal :
from flask.ext.restful import Resource, fields, marshal
topic_fields = {
'title': fields.String,
'content': fields.String,
'uri': fields.Url('topic'),
'creator': fields.String,
'created': fields.DateTime(dt_format='rfc822')
}
class TopicListApi(Resource):
def get(self):
return {'topics': [marshal(topic, topic_fields) for topic in DbTopic.query.all()]}
Vous devez lister explicitement ce que vous retournez et son type, ce que je préfère quand même pour une API. La sérialisation est facile à prendre en charge (pas besoin de jsonify
), les dates ne sont pas non plus un problème. Notez que le contenu du champ uri
est généré automatiquement en fonction du noeud final topic
et de l'id.
Je regarde ce problème depuis presque une journée, et voici ce que j’ai trouvé (crédit à https://stackoverflow.com/a/5249214/196358 pour avoir pointé moi dans cette direction).
(Remarque: j'utilise flask-sqlalchemy, mon format de déclaration de modèle est donc légèrement différent de sqlalchemy).
Dans mon models.py
fichier:
import json
class Serializer(object):
__public__ = None
"Must be implemented by implementors"
def to_serializable_dict(self):
dict = {}
for public_key in self.__public__:
value = getattr(self, public_key)
if value:
dict[public_key] = value
return dict
class SWEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Serializer):
return obj.to_serializable_dict()
if isinstance(obj, (datetime)):
return obj.isoformat()
return json.JSONEncoder.default(self, obj)
def SWJsonify(*args, **kwargs):
return current_app.response_class(json.dumps(dict(*args, **kwargs), cls=SWEncoder, indent=None if request.is_xhr else 2), mimetype='application/json')
# stolen from https://github.com/mitsuhiko/flask/blob/master/flask/helpers.py
et tous mes objets modèles ressemblent à ceci:
class User(db.Model, Serializer):
__public__ = ['id','username']
... field definitions ...
Dans mes vues, j'appelle SWJsonify partout où j'aurais appelé Jsonify
, comme ceci:
@app.route('/posts')
def posts():
posts = Post.query.limit(PER_PAGE).all()
return SWJsonify({'posts':posts })
Semble travailler assez bien. Même sur les relations. Je ne suis pas allé très loin avec ça, donc YMMV, mais jusqu'à présent, ça me semble plutôt "juste".
Suggestions bienvenues.
Voici ma réponse si vous utilisez la base déclarative (avec l'aide de certaines des réponses déjà postées):
# in your models definition where you define and extend declarative_base()
from sqlalchemy.ext.declarative import declarative_base
...
Base = declarative_base()
Base.query = db_session.query_property()
...
# define a new class (call "Model" or whatever) with an as_dict() method defined
class Model():
def as_dict(self):
return { c.name: getattr(self, c.name) for c in self.__table__.columns }
# and extend both the Base and Model class in your model definition, e.g.
class Rating(Base, Model):
____table= 'rating'
id = db.Column(db.Integer, primary_key=True)
fullurl = db.Column(db.String())
url = db.Column(db.String())
comments = db.Column(db.Text)
...
# then after you query and have a resultset (rs) of ratings
rs = Rating.query.all()
# you can jsonify it with
s = json.dumps([r.as_dict() for r in rs], default=alchemyencoder)
print (s)
# or if you have a single row
r = Rating.query.first()
# you can jsonify it with
s = json.dumps(r.as_dict(), default=alchemyencoder)
# you will need this alchemyencoder where your are calling json.dumps to handle datetime and decimal format
# credit to Joonas @ http://codeandlife.com/2014/12/07/sqlalchemy-results-to-json-the-easy-way/
def alchemyencoder(obj):
"""JSON encoder function for SQLAlchemy special classes."""
if isinstance(obj, datetime.date):
return obj.isoformat()
Elif isinstance(obj, decimal.Decimal):
return float(obj)
Voici un moyen d'ajouter une méthode as_dict () sur chaque classe, ainsi que toute autre méthode que vous souhaitez avoir sur chaque classe. Pas sûr que ce soit le moyen souhaité ou non, mais ça marche ...
class Base(object):
def as_dict(self):
return dict((c.name,
getattr(self, c.name))
for c in self.__table__.columns)
Base = declarative_base(cls=Base)
Je recherchais quelque chose comme l'approche Rails utilisée dans ActiveRecord to_json et implémentée à l'aide de ce Mixin après avoir été insatisfaite d'autres suggestions. Elle gère les modèles imbriqués et inclut ou exclut les attributs du niveau supérieur. ou des modèles imbriqués.
class Serializer(object):
def serialize(self, include={}, exclude=[], only=[]):
serialized = {}
for key in inspect(self).attrs.keys():
to_be_serialized = True
value = getattr(self, key)
if key in exclude or (only and key not in only):
to_be_serialized = False
Elif isinstance(value, BaseQuery):
to_be_serialized = False
if key in include:
to_be_serialized = True
nested_params = include.get(key, {})
value = [i.serialize(**nested_params) for i in value]
if to_be_serialized:
serialized[key] = value
return serialized
Ensuite, pour obtenir le BaseQuery sérialisable, j'ai étendu BaseQuery
class SerializableBaseQuery(BaseQuery):
def serialize(self, include={}, exclude=[], only=[]):
return [m.serialize(include, exclude, only) for m in self]
Pour les modèles suivants
class ContactInfo(db.Model, Serializer):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
full_name = db.Column(db.String())
source = db.Column(db.String())
source_id = db.Column(db.String())
email_addresses = db.relationship('EmailAddress', backref='contact_info', lazy='dynamic')
phone_numbers = db.relationship('PhoneNumber', backref='contact_info', lazy='dynamic')
class EmailAddress(db.Model, Serializer):
id = db.Column(db.Integer, primary_key=True)
email_address = db.Column(db.String())
type = db.Column(db.String())
contact_info_id = db.Column(db.Integer, db.ForeignKey('contact_info.id'))
class PhoneNumber(db.Model, Serializer):
id = db.Column(db.Integer, primary_key=True)
phone_number = db.Column(db.String())
type = db.Column(db.String())
contact_info_id = db.Column(db.Integer, db.ForeignKey('contact_info.id'))
phone_numbers = db.relationship('Invite', backref='phone_number', lazy='dynamic')
Vous pourriez faire quelque chose comme
@app.route("/contact/search", methods=['GET'])
def contact_search():
contact_name = request.args.get("name")
matching_contacts = ContactInfo.query.filter(ContactInfo.full_name.like("%{}%".format(contact_name)))
serialized_contact_info = matching_contacts.serialize(
include={
"phone_numbers" : {
"exclude" : ["contact_info", "contact_info_id"]
},
"email_addresses" : {
"exclude" : ["contact_info", "contact_info_id"]
}
}
)
return jsonify(serialized_contact_info)
Flask-Restful
0.3.6
l'analyse de la demande recommande Marshmallow
Marshmallow est une bibliothèque ORM/ODM/framework-agnostic permettant de convertir des types de données complexes, tels que des objets, en et de natif Python types de données.
Un exemple simple Marshmallow est présenté ci-dessous.
from Marshmallow import Schema, fields
class UserSchema(Schema):
name = fields.Str()
email = fields.Email()
created_at = fields.DateTime()
from Marshmallow import pprint
user = User(name="Monty", email="[email protected]")
schema = UserSchema()
result = schema.dump(user)
pprint(result)
# {"name": "Monty",
# "email": "[email protected]",
# "created_at": "2014-08-17T14:54:16.049594+00:00"}
Les fonctionnalités de base contiennent
Déclarer des schémas
Sérialiser des objets (“Dumping”)
Désérialisation d'objets ("Chargement")
Manipulation de collections d'objets
Validation
Spécification des noms d'attributs
Spécification des clés de sérialisation/désérialisation
Refactoring: Création de champ implicite
Sortie de commande
Champs "Lecture seule" et "Écriture seule"
Spécifier les valeurs de sérialisation/désérialisation par défaut
Schémas de nidification
Les champs personnalisés
Je travaillais avec une requête SQL defaultdict de listes d'objets RowProxy nommés jobDict. Il m'a fallu un certain temps pour déterminer le type de ces objets.
C’est un moyen rapide et très simple de résoudre le problème jsonEncoding propre en convertissant simplement la ligne en liste et en définissant d’abord le dict avec une valeur de liste.
jobDict = defaultdict(list)
def set_default(obj):
# trickyness needed here via import to know type
if isinstance(obj, RowProxy):
return list(obj)
raise TypeError
jsonEncoded = json.dumps(jobDict, default=set_default)
Je veux juste ajouter ma méthode pour le faire.
il suffit de définir un encodeur custome json pour sérifier vos modèles de base de données.
class ParentEncoder(json.JSONEncoder):
def default(self, obj):
# convert object to a dict
d = {}
if isinstance(obj, Parent):
return {"id": obj.id, "name": obj.name, 'children': list(obj.child)}
if isinstance(obj, Child):
return {"id": obj.id, "name": obj.name}
d.update(obj.__dict__)
return d
alors dans votre fonction de vue
parents = Parent.query.all()
dat = json.dumps({"data": parents}, cls=ParentEncoder)
resp = Response(response=dat, status=200, mimetype="application/json")
return (resp)
ça marche bien que le parent ait des relations