Serializzazione JSON dei modelli di Google App Engine


86

Ho cercato per un bel po 'di tempo senza successo. Il mio progetto non utilizza Django, esiste un modo semplice per serializzare i modelli di App Engine (google.appengine.ext.db.Model) in JSON o devo scrivere il mio serializzatore?

Modello:

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

Risposte:


62

Una semplice funzione ricorsiva può essere utilizzata per convertire un'entità (e qualsiasi referente) in un dizionario annidato che può essere passato a 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

2
C'è un piccolo errore nel codice: dove hai "output [key] = to_dict (model)" dovrebbe essere: "output [key] = to_dict (value)". Oltre a questo è perfetto. Grazie!
arikfr

1
Questo codice avrà esito negativo quando incontra una proprietà utente. Ho risolto il problema eseguendo "output [key] = str (value)" nell'altro finale, invece di sollevare un errore.
Boris Terzic

1
Roba fantastica. Un piccolo miglioramento consiste nell'usare invece iterkeys () dato che non usi "prop" lì.
PEZ

7
Non ho provato tutti i tipi possibili (data, GeoPt, ...), ma sembra che il datastore abbia esattamente questo metodo e finora ha funzionato per stringhe e numeri interi: developers.google.com/appengine/ docs / python / datastore /… Quindi non sono sicuro che sia necessario reinventare la ruota per serializzare su json:json.dumps(db.to_dict(Photo))
gentimouton

@gentimouton Questo metodo è una nuova aggiunta. Certamente non esisteva nel 2009
dmw

60

Questa è la soluzione più semplice che ho trovato. Richiede solo 3 righe di codici.

Aggiungi semplicemente un metodo al tuo modello per restituire un dizionario:

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

SimpleJSON ora funziona correttamente:

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

Hey, grazie per il consiglio. funziona benissimo tranne che non riesco a serializzare il campo della data. Ottengo: TypeError: datetime.datetime (2010, 5, 1, 9, 25, 22, 891937) non è serializzabile JSON
givp

Ciao, grazie per aver segnalato il problema. La soluzione è convertire l'oggetto data in una stringa. Ad esempio, puoi racchiudere la chiamata a "getattr (self, p)" con "unicode ()". Ho modificato il codice per riflettere questo.
mtgred

1
Per rimuovere i meta campi di db.Model, usa questo: dict ([(p, unicode (getattr (self, p))) for p in self.properties () if not p.startswith ("_")])
Wonil

per ndb, vedere la risposta di fredva.
Kenji Noguchi

self.properties () non ha funzionato per me. Ho usato self._properties. Riga completa: return dict ([(p, unicode (getattr (self, p))) for p in self._properties])
Eyal Levin

15

Nell'ultima versione (1.5.2) di App Engine SDK, è to_dict()stata introdotta una funzione che converte le istanze del modello in dizionari db.py. Vedere le note sulla versione .

Non c'è ancora alcun riferimento a questa funzione nella documentazione, ma l'ho provato io stesso e funziona come previsto.


Mi chiedo se questo sia stato rimosso? Ottengo AttributeError: 'module' object has no attribute 'to_dict'quando io from google.appengine.ext import dbe uso simplejson.dumps(db.to_dict(r))(dove r è un'istanza di una sottoclasse db.Model). Non vedo "to_dict" in google_appengine / google / appengine / ext / db / *
idbrii

1
deve essere usato come "db.to_dict (ObjectOfClassModel)"
Dmitry Dushkin

2
per un oggetto ndb, self.to_dict () fa il lavoro. Se vuoi rendere serializzabile la classe dal modulo json standard, aggiungi 'def default (self, o): return o.to_dict () `alla classe
Kenji Noguchi

7

Per serializzare i modelli, aggiungi un codificatore json personalizzato come nel seguente python:

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)

Questo codificherà:

  • una data come stringa isoformat ( secondo questo suggerimento ),
  • un modello come regola delle sue proprietà,
  • un utente come sua email.

Per decodificare la data puoi utilizzare questo javascript:

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

Nota: grazie all'utente pydave che ha modificato questo codice per renderlo più leggibile. Inizialmente avevo usato le espressioni if ​​/ else di Python per esprimere jsonEncoderin meno righe come segue: (Ho aggiunto alcuni commenti e ho usato google.appengine.ext.db.to_dict, per renderlo più chiaro dell'originale.)

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)

4

Non hai bisogno di scrivere il tuo "parser" (un parser presumibilmente trasformerebbe JSON in un oggetto Python), ma puoi ancora serializzare il tuo oggetto Python da solo.

Utilizzando simplejson :

import simplejson as json
serialized = json.dumps({
    'filename': self.filename,
    'title': self.title,
    'date_taken': date_taken.isoformat(),
    # etc.
})

1
Sì, ma non voglio doverlo fare per tutti i modelli. Sto cercando di trovare un approccio scalabile.
user111677

oh e sono davvero sorpreso di non riuscire a trovare alcuna best practice su questo. Pensavo che il modello del motore dell'app + rpc + json fosse un dato ...
user111677

4

Per i casi semplici, mi piace l'approccio sostenuto qui alla fine dell'articolo:

  # 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'articolo contiene anche, all'altra estremità dello spettro, una complessa classe di serializzatore che arricchisce django (e richiede _meta- non sono sicuro perché stai ricevendo errori su _meta mancante, forse il bug descritto qui ) con la capacità di serializzare computer proprietà / metodi. La maggior parte delle volte le esigenze di serializzazione si trovano da qualche parte nel mezzo, e per questi un approccio introspettivo come quello di @David Wilson potrebbe essere preferibile.


3

Anche se non stai usando django come framework, quelle librerie sono ancora disponibili per l'uso.

from django.core import serializers
data = serializers.serialize("xml", Photo.objects.all())

Volevi dire serializers.serialize ("json", ...)? Viene visualizzato "AttributeError: l'oggetto 'Photo' non ha attributo '_meta'". Cordiali saluti - serializers.serialize ("xml", Photo.objects.all ()) genera "AttributeError: il tipo di oggetto 'Photo' non ha attributo 'objects'". serializers.serialize ("xml", Photo.all ()) restituisce "SerializationError: oggetto non modello (<class 'model.Photo'>) rilevato durante la serializzazione".
user111677

2

Se usi app-engine-patch , dichiarerà automaticamente l' _metaattributo per te, e quindi potrai usaredjango.core.serializers come faresti normalmente sui modelli django (come nel codice di sledge).

App-engine-patch ha alcune altre fantastiche funzionalità come l'autenticazione ibrida (django + account google) e la parte amministrativa di django funziona.


qual è la differenza tra app-engine-patch e google-app-engine-django e la versione di django fornita con app engine python sdk? Da quello che ho capito, app-engine-patch è più completo?
user111677

Non ho provato la versione di django su app engine, ma penso che sia integrata così com'è. google-app-engine-django se non sbaglio cerca di far funzionare il modello di django con app-engine (con alcune limitazioni). app-engine-patch utilizza direttamente modelli di app-engine, aggiungono solo alcuni elementi minori. C'è un confronto tra i due sul loro sito web.
mtourne

2

La risposta di Mtgred sopra ha funzionato meravigliosamente per me: l'ho leggermente modificata in modo da poter ottenere anche la chiave per l'ingresso. Non come poche righe di codice, ma mi dà la chiave univoca:

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

2

Ho esteso la classe JSON Encoder scritta da dpatru per supportare:

  • Proprietà dei risultati della query (ad es. Car.owner_set)
  • ReferenceProperty: trasformalo in modo ricorsivo in JSON
  • Proprietà di filtro: solo le proprietà con un verbose_nameverranno codificate in 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)
    

2

Come menzionato da https://stackoverflow.com/users/806432/fredva , to_dict funziona alla grande. Ecco il mio codice che sto usando.

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

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

myJson = json.dumps(prepJson))

sì, e c'è anche un "to_dict" su Model ... questa funzione è la chiave per rendere l'intero problema così banale come dovrebbe essere. Funziona anche per NDB con proprietà "strutturate" e "ripetute"!
Nick Perkins

1

C'è un metodo, "Model.properties ()", definito per tutte le classi Model. Restituisce il dict che cerchi.

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

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

Vedi le proprietà del modello nella documentazione.


Alcuni oggetti non sono "serializzabili JSON":TypeError: <google.appengine.ext.db.StringProperty object at 0x4694550> is not JSON serializable
idbrii

1

Queste API (google.appengine.ext.db) non sono più consigliate. Le app che utilizzano queste API possono essere eseguite solo nel runtime Python 2 di App Engine e dovranno migrare ad altre API e servizi prima di migrare al runtime Python 3 di App Engine. Per saperne di più: clicca qui


0

Per serializzare un'istanza del modello Datastore non è possibile utilizzare json.dumps (non è stato testato ma Lorenzo lo ha indicato). Forse in futuro funzionerà quanto segue.

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)

la domanda riguarda la conversione di un'istanza del modello Datastore AppEngine in JSON. La tua soluzione riguarda solo la conversione di un dizionario Python in JSON
sintonizzato il

@tunedconsulting Non ho provato a serializzare un'istanza del modello Datastore con json.dumps ma presumo che funzionerebbe con qualsiasi oggetto. Un bug report dovrebbe essere inviato se non è come la documentazione afferma che json.dumps accetta un oggetto come parametro. Viene aggiunto come commento con un solo commento re che non esisteva nel 2009. Aggiunta questa risposta perché sembra un po 'datata ma se non funzionasse, sono felice di rimuoverla.
HMR

1
Se provi a json.dumps un oggetto entità o una classe modello , ottieni TypeError: 'non è serializzabile JSON' <Object at 0x0xxxxxx>. Datastore di GAE ha i propri tipi di dati (ad esempio per le date). L'attuale risposta giusta, testata e funzionante, è quella di dmw che trasforma alcuni tipi di dati problematici in serializzabili.
sintonizzato il

@tunedconsulting Grazie per il tuo contributo, aggiornerò la mia risposta.
HMR
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.