web-dev-qa-db-fra.com

jsonify un résultat SQLAlchemy défini dans Flask

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
109
mal-wan

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 ).

154
plaes

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__)
36
bitcycle

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))
34
Carl Ekerot

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

17
n0nSmoker

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 dicts:

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()))
5
robru

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)
4
reubano

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.

4
Adversus

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.

2
Kenny Winker

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)
1
VinnyQ77

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)
1
tahoe

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)
1
zwalker

Flask-Restful0.3.6l'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

1
Shihe Zhang

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)
0
Nick

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

0
tyan