Serialización JSON de modelos de Google App Engine

86

He estado buscando durante bastante tiempo sin éxito. Mi proyecto no usa Django, ¿existe una forma sencilla de serializar los modelos de App Engine (google.appengine.ext.db.Model) en JSON o necesito escribir mi propio serializador?

Modelo:

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

Respuestas:

62

Se puede usar una función recursiva simple para convertir una entidad (y cualquier referente) en un diccionario anidado que se puede pasar 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
dmw
fuente
2
Hay un pequeño error en el código: donde tienes "salida [clave] = to_dict (modelo)" debería ser: "salida [clave] = to_dict (valor)". Además de eso, es perfecto. ¡Gracias!
arikfr
1
Este código fallará cuando encuentre una propiedad de usuario. Lo solucioné haciendo "output [key] = str (value)" en el else final, en lugar de generar un error.
Boris Terzic
1
Buena cosa. Una pequeña mejora es usar iterkeys () en su lugar, ya que no usa "prop" allí.
PEZ
7
No probé todos los tipos posibles (fecha, GeoPt, ...), pero parece que el almacén de datos tiene exactamente este método, y hasta ahora me ha estado funcionando para cadenas y enteros: developers.google.com/appengine/ docs / python / datastore /… Así que no estoy seguro de que necesite reinventar la rueda para serializarla en json:json.dumps(db.to_dict(Photo))
gentimouton
@gentimouton Ese método es una nueva adición. Ciertamente no existía en 2009
dmw
60

Esta es la solución más simple que encontré. Requiere solo 3 líneas de códigos.

Simplemente agregue un método a su modelo para devolver un diccionario:

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

SimpleJSON ahora funciona correctamente:

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
fuente
hola gracias por la propina. esto funciona muy bien, excepto que parece que no puedo serializar el campo de fecha. Me sale: TypeError: datetime.datetime (2010, 5, 1, 9, 25, 22, 891937) no es serializable JSON
givp
Hola, gracias por señalar el problema. La solución es convertir el objeto de fecha en una cadena. Por ejemplo, puede ajustar la llamada a "getattr (self, p)" con "unicode ()". Edité el código para reflejar esto.
mtgred
1
Para eliminar los metacampos de db.Model, use esto: dict ([(p, unicode (getattr (self, p))) para p en self.properties () si no p.startswith ("_")])
Wonil
para ndb, vea la respuesta de fredva.
Kenji Noguchi
self.properties () no funcionó para mí. Usé self._properties. Línea completa: return dict ([(p, unicode (getattr (self, p))) para p en self._properties])
Eyal Levin
15

En la última versión (1.5.2) del SDK de App Engine, to_dict()se introdujo una función que convierte instancias de modelo en diccionarios db.py. Consulte las notas de la versión .

No hay ninguna referencia a esta función en la documentación hasta el momento, pero la he probado yo mismo y funciona como se esperaba.

fredrikmorken
fuente
Me pregunto si esto se ha eliminado. Obtengo AttributeError: 'module' object has no attribute 'to_dict'cuando I from google.appengine.ext import dby uso simplejson.dumps(db.to_dict(r))(donde r es una instancia de una subclase db.Model). No veo "to_dict" en google_appengine / google / appengine / ext / db / *
idbrii
1
tiene que ser utilizado como "db.to_dict (ObjectOfClassModel)"
Dmitry Dushkin
2
para un objeto ndb, self.to_dict () hace el trabajo. Si desea que la clase sea serializable por el módulo json estándar, agregue 'def default (self, o): return o.to_dict () `a la clase
Kenji Noguchi
7

Para serializar modelos, agregue un codificador json personalizado como en el siguiente 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)

Esto codificará:

  • una fecha como una cadena de isoformato ( según esta sugerencia ),
  • un modelo como un dictado de sus propiedades,
  • un usuario como su correo electrónico.

Para decodificar la fecha puedes usar este javascript:

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

Nota: Gracias al usuario pydave que editó este código para hacerlo más legible. Originalmente había usado las expresiones if / else de python para expresar jsonEncoderen menos líneas de la siguiente manera: (agregué algunos comentarios y usé google.appengine.ext.db.to_dict, para hacerlo más claro que el 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
fuente
4

No necesitas escribir tu propio "analizador" (un analizador presumiblemente convertiría JSON en un objeto Python), pero aún puedes serializar tu objeto Python tú mismo.

Usando simplejson :

import simplejson as json
serialized = json.dumps({
    'filename': self.filename,
    'title': self.title,
    'date_taken': date_taken.isoformat(),
    # etc.
})
Jonathan Feinberg
fuente
1
Sí, pero no quiero tener que hacer esto para todos los modelos. Estoy tratando de encontrar un enfoque escalable.
user111677
Ah, y estoy realmente sorprendido de que no pueda encontrar ninguna de las mejores prácticas al respecto. Pensé que el modelo de motor de la aplicación + rpc + json era un hecho ...
user111677
4

Para casos simples, me gusta el enfoque propuesto aquí al final del artículo:

  # 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())})

El artículo también contiene, en el otro extremo del espectro, una clase de serializador compleja que enriquece la de django (y requiere _meta, no estoy seguro de por qué está obteniendo errores sobre la falta de _meta, tal vez el error descrito aquí ) con la capacidad de serializar calculado propiedades / métodos. La mayoría de las veces, las necesidades de serialización se encuentran en algún punto intermedio, y para aquellos, puede ser preferible un enfoque introspectivo como el de @David Wilson.

Alex Martelli
fuente
3

Incluso si no está utilizando django como marco, esas bibliotecas todavía están disponibles para su uso.

from django.core import serializers
data = serializers.serialize("xml", Photo.objects.all())
Andrew Sledge
fuente
¿Quiso decir serializers.serialize ("json", ...)? Eso arroja "AttributeError: el objeto 'Photo' no tiene atributo '_meta'". FYI - serializers.serialize ("xml", Photo.objects.all ()) arroja "AttributeError: el objeto de tipo 'Photo' no tiene el atributo 'objects'". serializers.serialize ("xml", Photo.all ()) arroja "SerializationError: Objeto no modelo (<class 'model.Photo'>) encontrado durante la serialización".
user111677
2

Si usa app-engine-patch , declarará automáticamente el _metaatributo y luego podrá usardjango.core.serializers como lo haría normalmente en los modelos de django (como en el código de sledge).

App-engine-patch tiene algunas otras características interesantes, como una autenticación híbrida (cuentas de django + google), y la parte de administración de django funciona.

mtourne
fuente
¿Cuál es la diferencia entre app-engine-patch vs google-app-engine-django vs la versión de django enviada con app engine python sdk? Por lo que tengo entendido, ¿app-engine-patch es más completo?
user111677
No he probado la versión de django en el motor de la aplicación, pero creo que está integrado tal cual. google-app-engine-django, si no me equivoco, intenta hacer que el modelo de django funcione con app-engine (con algunas limitaciones). app-engine-patch usa directamente modelos de app-engine, solo le agregan algunas cosas menores. Hay una comparación entre los dos en su sitio web.
mtourne
2

La respuesta de Mtgred anterior funcionó de maravilla para mí: la modifiqué ligeramente para que también pudiera obtener la clave para la entrada. No como pocas líneas de código, pero me da la clave única:

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
fuente
2

He extendido la clase JSON Encoder escrita por dpatru para admitir:

  • Propiedades de los resultados de la consulta (por ejemplo, car.owner_set)
  • ReferenceProperty: conviértalo de forma recursiva en JSON
  • Propiedades de filtrado: solo las propiedades con un verbose_namese codificarán 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
fuente
2

Como lo menciona https://stackoverflow.com/users/806432/fredva , el to_dict funciona muy bien. Aquí está mi código que estoy usando.

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

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

myJson = json.dumps(prepJson))
TrentVB
fuente
sí, y también hay un "to_dict" en Model ... esta función es la clave para hacer que todo este problema sea tan trivial como debería ser. ¡Incluso funciona para NDB con propiedades "estructuradas" y "repetidas"!
Nick Perkins
1

Hay un método, "Model.properties ()", definido para todas las clases de modelos. Devuelve el dict que buscas.

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

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

Consulte Propiedades del modelo en los documentos.

Jeff Carollo
fuente
Algunos objetos no son "JSON serializables":TypeError: <google.appengine.ext.db.StringProperty object at 0x4694550> is not JSON serializable
idbrii
1

Estas API (google.appengine.ext.db) ya no se recomiendan. Las aplicaciones que usan estas API solo pueden ejecutarse en el tiempo de ejecución de App Engine Python 2 y deberán migrar a otras API y servicios antes de migrar al tiempo de ejecución de App Engine Python 3. Para saber más: haga clic aquí

Rishabh Jain
fuente
0

Para serializar una instancia de modelo de almacén de datos, no puede usar json.dumps (no lo he probado, pero Lorenzo lo señaló). Quizás en el futuro lo siguiente funcione.

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
fuente
la pregunta es sobre la conversión de una instancia del modelo de almacén de datos de AppEngine a JSON. Su solución es solo convertir un diccionario de Python a JSON
ajustado el
@tunedconsulting No he intentado serializar una instancia de modelo de almacén de datos con json.dumps, pero supongo que funcionaría con cualquier objeto. Se debe enviar un informe de error si no es así, ya que la documentación indica que json.dumps toma un objeto como parámetro. Se agrega como un comentario con solo volver a comentar que no existía en 2009. Agregué esta respuesta porque parece un poco desactualizada, pero si no funciona, me complace eliminarla.
HMR
1
Si intenta json.dumps un objeto de entidad o una clase de modelo , obtiene TypeError: 'no es JSON serializable' <Object at 0x0xxxxxx>. El almacén de datos de GAE tiene sus propios tipos de datos (para fechas, por ejemplo). La respuesta correcta actual, probada y funcionando, es la de dmw que transforma algunos tipos de datos problemáticos en serializables.
sintonizado el
@tunedconsulting Gracias por su opinión sobre esto, actualizaré mi respuesta.
HMR