Sérialisation JSON des modèles Google App Engine

86

Je cherche depuis pas mal de temps sans succès. Mon projet n'utilise pas Django, existe-t-il un moyen simple de sérialiser les modèles App Engine (google.appengine.ext.db.Model) en JSON ou dois-je écrire mon propre sérialiseur?

Modèle:

class Photo(db.Model):
    filename = db.StringProperty()
    title = db.StringProperty()
    description = db.StringProperty(multiline=True)
    date_taken = db.DateTimeProperty()
    date_uploaded = db.DateTimeProperty(auto_now_add=True)
    album = db.ReferenceProperty(Album, collection_name='photo')
user111677
la source

Réponses:

62

Une simple fonction récursive peut être utilisée pour convertir une entité (et tous les référents) en un dictionnaire imbriqué qui peut être passé à simplejson:

import datetime
import time

SIMPLE_TYPES = (int, long, float, bool, dict, basestring, list)

def to_dict(model):
    output = {}

    for key, prop in model.properties().iteritems():
        value = getattr(model, key)

        if value is None or isinstance(value, SIMPLE_TYPES):
            output[key] = value
        elif isinstance(value, datetime.date):
            # Convert date/datetime to MILLISECONDS-since-epoch (JS "new Date()").
            ms = time.mktime(value.utctimetuple()) * 1000
            ms += getattr(value, 'microseconds', 0) / 1000
            output[key] = int(ms)
        elif isinstance(value, db.GeoPt):
            output[key] = {'lat': value.lat, 'lon': value.lon}
        elif isinstance(value, db.Model):
            output[key] = to_dict(value)
        else:
            raise ValueError('cannot encode ' + repr(prop))

    return output
dmw
la source
2
Il y a une petite erreur dans le code: là où vous avez "output [key] = to_dict (model)", cela devrait être: "output [key] = to_dict (value)". En plus, c'est parfait. Merci!
arikfr
1
Ce code échouera lorsqu'il rencontrera un UserProperty. Je l'ai contourné en faisant "output [key] = str (value)" dans le final else, au lieu de soulever une erreur.
Boris Terzic
1
Super truc. Une petite amélioration est d'utiliser iterkeys () à la place puisque vous n'utilisez pas "prop" ici.
PEZ
7
Je n'ai pas essayé tous les types possibles (date, GeoPt, ...), mais il semble que le magasin de données utilise exactement cette méthode, et cela fonctionne jusqu'à présent pour les chaînes et les entiers: développeurs.google.com / appengine/ docs / python / datastore /… Donc je ne suis pas sûr que vous ayez besoin de réinventer la roue pour sérialiser en json:json.dumps(db.to_dict(Photo))
gentimouton
@gentimouton Cette méthode est un nouvel ajout. Il n'existait certainement pas en 2009
dmw
60

C'est la solution la plus simple que j'ai trouvée. Il ne nécessite que 3 lignes de codes.

Ajoutez simplement une méthode à votre modèle pour renvoyer un dictionnaire:

class DictModel(db.Model):
    def to_dict(self):
       return dict([(p, unicode(getattr(self, p))) for p in self.properties()])

SimpleJSON fonctionne désormais correctement:

class Photo(DictModel):
   filename = db.StringProperty()
   title = db.StringProperty()
   description = db.StringProperty(multiline=True)
   date_taken = db.DateTimeProperty()
   date_uploaded = db.DateTimeProperty(auto_now_add=True)
   album = db.ReferenceProperty(Album, collection_name='photo')

from django.utils import simplejson
from google.appengine.ext import webapp

class PhotoHandler(webapp.RequestHandler):
   def get(self):
      photos = Photo.all()
      self.response.out.write(simplejson.dumps([p.to_dict() for p in photos]))
mtgred
la source
hé merci pour le tuyau. cela fonctionne très bien sauf que je n'arrive pas à sérialiser le champ de date. J'obtiens: TypeError: datetime.datetime (2010, 5, 1, 9, 25, 22, 891937) n'est pas sérialisable JSON
givp
Salut, merci d'avoir signalé le problème. La solution consiste à convertir l'objet date en chaîne. Par exemple, vous pouvez encapsuler l'appel à "getattr (self, p)" avec "unicode ()". J'ai modifié le code pour refléter cela.
mtgred
1
Pour supprimer les champs méta de db.Model, utilisez ceci: dict ([(p, unicode (getattr (self, p))) for p dans self.properties () sinon p.startswith ("_")])
Wonil
pour ndb, voir la réponse de fredva.
Kenji Noguchi
self.properties () n'a pas fonctionné pour moi. J'ai utilisé self._properties. Ligne complète: return dict ([(p, unicode (getattr (self, p))) for p in self._properties])
Eyal Levin
15

Dans la dernière version (1.5.2) du SDK App Engine, une to_dict()fonction qui convertit les instances de modèle en dictionnaires a été introduite dans db.py. Voir les notes de version .

Il n'y a pas encore de référence à cette fonction dans la documentation, mais je l'ai essayé moi-même et cela fonctionne comme prévu.

fredrikmorken
la source
Je me demande si cela a été supprimé? Je reçois AttributeError: 'module' object has no attribute 'to_dict'quand je from google.appengine.ext import dbet l' utilisation simplejson.dumps(db.to_dict(r))(où r est une instance d'une sous - classe db.Model). Je ne vois pas "to_dict" dans google_appengine / google / appengine / ext / db / *
idbrii
1
il doit être utilisé comme "db.to_dict (ObjectOfClassModel)"
Dmitry Dushkin
2
pour un objet ndb, self.to_dict () fait le travail. Si vous voulez rendre la classe sérialisable par le module json standard, ajoutez 'def default (self, o): return o.to_dict () `à la classe
Kenji Noguchi
7

Pour sérialiser des modèles, ajoutez un encodeur json personnalisé comme dans le python suivant:

import datetime
from google.appengine.api import users
from google.appengine.ext import db
from django.utils import simplejson

class jsonEncoder(simplejson.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            return obj.isoformat()

        elif isinstance(obj, db.Model):
            return dict((p, getattr(obj, p)) 
                        for p in obj.properties())

        elif isinstance(obj, users.User):
            return obj.email()

        else:
            return simplejson.JSONEncoder.default(self, obj)


# use the encoder as: 
simplejson.dumps(model, cls=jsonEncoder)

Cela encodera:

  • une date sous forme de chaîne isoformat ( selon cette suggestion ),
  • un modèle comme un dict de ses propriétés,
  • un utilisateur comme son email.

Pour décoder la date, vous pouvez utiliser ce javascript:

function decodeJsonDate(s){
  return new Date( s.slice(0,19).replace('T',' ') + ' GMT' );
} // Note that this function truncates milliseconds.

Remarque: Merci à l'utilisateur pydave qui a édité ce code pour le rendre plus lisible. J'avais initialement utilisé les expressions if / else de python pour exprimer jsonEncoderen moins de lignes comme suit: (J'ai ajouté quelques commentaires et utilisé google.appengine.ext.db.to_dict, pour le rendre plus clair que l'original.)

class jsonEncoder(simplejson.JSONEncoder):
  def default(self, obj):
    isa=lambda x: isinstance(obj, x) # isa(<type>)==True if obj is of type <type>
    return obj.isoformat() if isa(datetime.datetime) else \
           db.to_dict(obj) if isa(db.Model) else \
           obj.email()     if isa(users.User) else \
           simplejson.JSONEncoder.default(self, obj)
Daniel Patru
la source
4

Vous n'avez pas besoin d'écrire votre propre "analyseur" (un analyseur transforme vraisemblablement JSON en objet Python), mais vous pouvez toujours sérialiser votre objet Python vous-même.

En utilisant simplejson :

import simplejson as json
serialized = json.dumps({
    'filename': self.filename,
    'title': self.title,
    'date_taken': date_taken.isoformat(),
    # etc.
})
Jonathan Feinberg
la source
1
Oui, mais je ne veux pas avoir à faire cela pour chaque modèle. J'essaie de trouver une approche évolutive.
user111677
oh et je suis vraiment surpris de ne trouver aucune meilleure pratique à ce sujet. Je pensais que le modèle de moteur d'application + rpc + json était une donnée ...
user111677
4

Pour les cas simples, j'aime l'approche préconisée ici à la fin de l'article:

  # after obtaining a list of entities in some way, e.g.:
  user = users.get_current_user().email().lower();
  col = models.Entity.gql('WHERE user=:1',user).fetch(300, 0)

  # ...you can make a json serialization of name/key pairs as follows:
  json = simplejson.dumps(col, default=lambda o: {o.name :str(o.key())})

L'article contient également, à l'autre bout du spectre, une classe de sérialiseur complexe qui enrichit ceux de django (et ne nécessite _metapas - je ne sais pas pourquoi vous obtenez des erreurs sur _meta manquant, peut-être le bogue décrit ici ) avec la possibilité de sérialiser calculé propriétés / méthodes. La plupart du temps, vos besoins de sérialisation se situent quelque part entre les deux, et pour ceux-ci, une approche introspective telle que celle de @David Wilson peut être préférable.

Alex Martelli
la source
3

Même si vous n'utilisez pas django comme framework, ces bibliothèques sont toujours à votre disposition.

from django.core import serializers
data = serializers.serialize("xml", Photo.objects.all())
Andrew Sledge
la source
Voulez-vous dire serializers.serialize ("json", ...)? Cela lance "AttributeError: l'objet 'Photo' n'a pas d'attribut '_meta'". FYI - serializers.serialize ("xml", Photo.objects.all ()) lance "AttributeError: l'objet de type 'Photo' n'a pas d'attribut 'objets'". serializers.serialize ("xml", Photo.all ()) lance "SerializationError: Objet non-modèle (<class 'model.Photo'>) rencontré pendant la sérialisation".
user111677
2

Si vous utilisez app-engine-patch, il déclarera automatiquement l' _metaattribut pour vous, puis vous pourrez utiliserdjango.core.serializers comme vous le feriez normalement sur les modèles django (comme dans le code de sledge).

App-engine-patch a quelques autres fonctionnalités intéressantes telles qu'une authentification hybride (comptes django + google) et la partie admin de django fonctionne.

mtourne
la source
Quelle est la différence entre app-engine-patch et google-app-engine-django et la version django livrée avec le moteur d'application python sdk? D'après ce que je comprends, app-engine-patch est plus complet?
user111677
Je n'ai pas essayé la version de django sur le moteur d'application, mais je pense qu'elle est intégrée telle quelle. google-app-engine-django si je ne me trompe pas, essaie de faire fonctionner le modèle de django avec app-engine (avec quelques limitations). app-engine-patch utilise directement des modèles de moteur d'application, ils y ajoutent simplement quelques éléments mineurs. Il y a une comparaison entre les deux sur leur site Web.
mtourne
2

La réponse de Mtgred ci-dessus a fonctionné à merveille pour moi - je l'ai légèrement modifiée pour que je puisse également obtenir la clé de l'entrée. Pas aussi peu de lignes de code, mais cela me donne la clé unique:

class DictModel(db.Model):
def to_dict(self):
    tempdict1 = dict([(p, unicode(getattr(self, p))) for p in self.properties()])
    tempdict2 = {'key':unicode(self.key())}
    tempdict1.update(tempdict2)
    return tempdict1
Victor Van Hee
la source
2

J'ai étendu la classe JSON Encoder écrite par dpatru pour prendre en charge:

  • Propriétés des résultats de la requête (par exemple, car.owner_set)
  • ReferenceProperty - le transformer récursivement en JSON
  • Propriétés de filtrage - seules les propriétés avec un verbose_nameseront encodées en JSON

    class DBModelJSONEncoder(json.JSONEncoder):
        """Encodes a db.Model into JSON"""
    
        def default(self, obj):
            if (isinstance(obj, db.Query)):
                # It's a reference query (holding several model instances)
                return [self.default(item) for item in obj]
    
            elif (isinstance(obj, db.Model)):
                # Only properties with a verbose name will be displayed in the JSON output
                properties = obj.properties()
                filtered_properties = filter(lambda p: properties[p].verbose_name != None, properties)
    
                # Turn each property of the DB model into a JSON-serializeable entity
                json_dict = dict([(
                        p,
                        getattr(obj, p)
                            if (not isinstance(getattr(obj, p), db.Model))
                            else
                        self.default(getattr(obj, p)) # A referenced model property
                    ) for p in filtered_properties])
    
                json_dict['id'] = obj.key().id() # Add the model instance's ID (optional - delete this if you do not use it)
    
                return json_dict
    
            else:
                # Use original JSON encoding
                return json.JSONEncoder.default(self, obj)
    
Yaron Budowski
la source
2

Comme mentionné par https://stackoverflow.com/users/806432/fredva , le to_dict fonctionne très bien. Voici mon code que j'utilise.

foos = query.fetch(10)
prepJson = []

for f in foos:
  prepJson.append(db.to_dict(f))

myJson = json.dumps(prepJson))
TrentVB
la source
oui, et il y a aussi un "to_dict" sur Model ... cette fonction est la clé pour rendre tout ce problème aussi trivial qu'il devrait l'être. Cela fonctionne même pour NDB avec des propriétés "structurées" et "répétées"!
Nick Perkins
1

Il existe une méthode, "Model.properties ()", définie pour toutes les classes Model. Il renvoie le dict que vous recherchez.

from django.utils import simplejson
class Photo(db.Model):
  # ...

my_photo = Photo(...)
simplejson.dumps(my_photo.properties())

Voir Propriétés du modèle dans la documentation.

Jeff Carollo
la source
Certains objets ne sont pas "sérialisables JSON":TypeError: <google.appengine.ext.db.StringProperty object at 0x4694550> is not JSON serializable
idbrii
1

Ces API (google.appengine.ext.db) ne sont plus recommandées. Les applications qui utilisent ces API ne peuvent s'exécuter que dans l'environnement d'exécution App Engine Python 2 et devront migrer vers d'autres API et services avant de migrer vers l'environnement d'exécution App Engine Python 3. Pour en savoir plus: cliquez ici

Rishabh Jain
la source
0

Pour sérialiser une instance de modèle Datastore, vous ne pouvez pas utiliser json.dumps (pas testé mais Lorenzo l'a signalé). Peut-être qu'à l'avenir, ce qui suit fonctionnera.

http://docs.python.org/2/library/json.html

import json
string = json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}])
object = json.loads(self.request.body)
HMR
la source
la question concerne la conversion d'une instance de modèle AppEngine Datastore en JSON. Votre solution consiste uniquement à convertir un dictionnaire Python en JSON
réglé le
@tunedconsulting Je n'ai pas essayé de sérialiser une instance de modèle Datastore avec json.dumps, mais je suppose que cela fonctionnerait avec n'importe quel objet. Un rapport de bogue doit être soumis si ce n'est pas ce que la documentation indique que json.dumps prend un objet comme paramètre. Il est ajouté en commentaire avec seulement un commentaire sur le fait qu'il n'existait pas en 2009. Ajout de cette réponse car elle semble un peu dépassée mais si cela ne fonctionnait pas, je serai heureux de la supprimer.
HMR
1
Si vous essayez de json.dumps un objet entité ou une classe de modèle , vous obtenez TypeError: 'is not JSON serializable' <Object at 0x0xxxxxx>. La banque de données de GAE a ses propres types de données (pour les dates par exemple). La bonne réponse actuelle, testée et fonctionnelle, est celle de dmw qui transforme certains types de données problématiques en types sérialisables.
accordé le
@tunedconsulting Merci pour votre contribution à ce sujet, je mettrai à jour ma réponse.
HMR