Serialização JSON de modelos do Google App Engine


86

Venho procurando há um bom tempo sem sucesso. Meu projeto não está usando Django, existe uma maneira simples de serializar modelos do App Engine (google.appengine.ext.db.Model) em JSON ou preciso escrever meu próprio 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')

Respostas:


62

Uma função recursiva simples pode ser usada para converter uma entidade (e quaisquer referentes) em um dicionário aninhado que pode ser passado para 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
Há um pequeno erro no código: Onde você tem "output [key] = to_dict (model)", deveria ser: "output [key] = to_dict (value)". Além disso, é perfeito. Obrigado!
arikfr

1
Este código falhará quando encontrar uma UserProperty. Eu trabalhei em torno disso fazendo "output [key] = str (value)" no else final, em vez de levantar um erro.
Boris Terzic

1
Coisas boas. Uma pequena melhoria é usar iterkeys (), uma vez que você não usa "prop" lá.
PEZ

7
Não tentei todos os tipos possíveis (data, GeoPt, ...), mas parece que o armazenamento de dados tem exatamente este método e tem funcionado com strings e inteiros para mim até agora: developers.google.com/appengine/ docs / python / datastore /… Então, não tenho certeza se você precisa reinventar a roda para serializar para json:json.dumps(db.to_dict(Photo))
gentimouton

@gentimouton Esse método é uma nova adição. Certamente não existia em 2009
dmw

60

Esta é a solução mais simples que encontrei. Requer apenas 3 linhas de códigos.

Basta adicionar um método ao seu modelo para retornar um dicionário:

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

SimpleJSON agora funciona corretamente:

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, obrigado pela dica. isso funciona muito bem, exceto que não consigo serializar o campo de data. Eu obtenho: TypeError: datetime.datetime (2010, 5, 1, 9, 25, 22, 891937) não é serializável em JSON
dado

Oi, obrigado por apontar o problema. A solução é converter o objeto de data em uma string. Por exemplo, você pode encerrar a chamada para "getattr (self, p)" com "unicode ()". Eu editei o código para refletir isso.
mtgred de

1
Para remover os metacampos de db.Model, use: dict ([(p, unicode (getattr (self, p))) para p em self.properties () se não p.startswith ("_")])
Wonil

para ndb, consulte a resposta de fredva.
Kenji Noguchi

self.properties () não funcionou para mim. Eu usei self._properties. Linha completa: return dict ([(p, unicode (getattr (self, p))) para p em self._properties])
Eyal Levin

15

Na versão mais recente (1.5.2) do SDK do App Engine, uma to_dict()função que converte instâncias de modelo em dicionários foi introduzida em db.py. Veja as notas de lançamento .

Não há referência a essa função na documentação ainda, mas eu mesmo tentei e funciona conforme o esperado.


Eu me pergunto se isso foi removido? Eu entendo AttributeError: 'module' object has no attribute 'to_dict'quando eu from google.appengine.ext import dbuso simplejson.dumps(db.to_dict(r))(onde r é uma instância de uma subclasse db.Model). Não vejo "to_dict" em google_appengine / google / appengine / ext / db / *
idbrii

1
deve ser usado como "db.to_dict (ObjectOfClassModel)"
Dmitry Dushkin,

2
para um objeto ndb, self.to_dict () faz o trabalho. Se você quiser tornar a classe serializável pelo módulo json padrão, adicione 'def default (self, o): return o.to_dict () `para a classe
Kenji Noguchi

7

Para serializar modelos, adicione um codificador json personalizado como no seguinte 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)

Isso irá codificar:

  • uma data como string isoformat ( por esta sugestão ),
  • um modelo como um ditado de suas propriedades,
  • um usuário como seu e-mail.

Para decodificar a data, você pode usar este javascript:

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

Nota: Obrigado ao usuário pydave que editou este código para torná-lo mais legível. Eu tinha originalmente usado as expressões if / else do python para expressar jsonEncoderem menos linhas, como segue: (Eu adicionei alguns comentários e usei google.appengine.ext.db.to_dict, para torná-lo mais claro do que o 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)

4

Você não precisa escrever seu próprio "analisador" (um analisador provavelmente transformaria JSON em um objeto Python), mas você ainda pode serializar seu objeto Python sozinho.

Usando o simplejson :

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

1
Sim, mas não quero ter que fazer isso para todos os modelos. Estou tentando encontrar uma abordagem escalonável.
user111677

ah, estou realmente surpreso por não conseguir encontrar as melhores práticas sobre isso. Achei que o modelo do App Engine + rpc + json fosse um dado ...
user111677

4

Para casos simples, gosto da abordagem defendida aqui no final do artigo:

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

O artigo também contém, na outra extremidade do espectro, uma classe serializadora complexa que enriquece o django (e requer _meta- não tenho certeza porque você está recebendo erros sobre _meta faltando, talvez o bug descrito aqui ) com a capacidade de serializar computado propriedades / métodos. Na maioria das vezes, você precisa de serialização em algum ponto intermediário, e para aqueles uma abordagem introspectiva como @David Wilson pode ser preferível.


3

Mesmo se você não estiver usando o django como estrutura, essas bibliotecas ainda estão disponíveis para você usar.

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

Você quis dizer serializers.serialize ("json", ...)? Isso gera "AttributeError: 'Photo' objeto não tem atributo '_meta'". FYI - serializers.serialize ("xml", Photo.objects.all ()) lança "AttributeError: objeto de tipo 'Foto' não tem atributo 'objetos'". serializers.serialize ("xml", Photo.all ()) lança "SerializationError: Objeto não modelo (<class 'model.Photo'>) encontrado durante a serialização".
user111677

2

Se você usar app-engine-patch, ele declarará automaticamente o _metaatributo para você, e então você pode usar django.core.serializerscomo faria normalmente em modelos django (como no código do sledge).

O patch do App-engine possui alguns outros recursos interessantes, como autenticação híbrida (django + contas do Google) e a parte administrativa do django funciona.


qual é a diferença entre app-engine-patch vs google-app-engine-django vs a versão django fornecida com app engine python sdk? Pelo que entendi, app-engine-patch é mais completo?
user111677

Não experimentei a versão do django no App Engine, mas acho que está integrado como está. google-app-engine-django se não me engano tenta fazer o modelo do django funcionar com app-engine (com algumas limitações). app-engine-patch usa modelos de app-engine diretamente, eles apenas adicionam alguns itens menores a ele. Há uma comparação entre os dois em seu site.
mtourne,

2

A resposta de Mtgred acima funcionou maravilhosamente para mim - modifiquei ligeiramente para que também pudesse obter a chave da entrada. Não como algumas linhas de código, mas me dá a chave ú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

2

Estendi a classe JSON Encoder escrita por dpatru para oferecer suporte a:

  • Propriedades dos resultados da consulta (por exemplo, car.owner_set)
  • ReferenceProperty - transforma recursivamente em JSON
  • Propriedades de filtragem - apenas propriedades com um verbose_nameserão codificadas em 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

Conforme mencionado por https://stackoverflow.com/users/806432/fredva , o to_dict funciona muito bem. Aqui está o meu código que estou usando.

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

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

myJson = json.dumps(prepJson))

sim, e também há um "to_dict" no Model ... esta função é a chave para tornar todo esse problema tão trivial quanto deveria ser. Ele funciona até mesmo para NDB com propriedades "estruturadas" e "repetidas"!
Nick Perkins

1

Existe um método, "Model.properties ()", definido para todas as classes de Model. Ele retorna o dict que você procura.

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

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

Consulte as propriedades do modelo nos documentos.


Alguns objetos não são "serializáveis ​​JSON":TypeError: <google.appengine.ext.db.StringProperty object at 0x4694550> is not JSON serializable
idbrii

1

Essas APIs (google.appengine.ext.db) não são mais recomendadas. Os aplicativos que usam essas APIs só podem ser executados no tempo de execução do App Engine Python 2 e precisarão migrar para outras APIs e serviços antes de migrar para o tempo de execução do App Engine Python 3. Para saber mais: clique aqui


0

Para serializar uma instância do Datastore Model, você não pode usar json.dumps (não testei, mas Lorenzo apontou). Talvez no futuro o seguinte 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)

a questão é sobre como converter uma instância de modelo do AppEngine Datastore em JSON. Sua solução é apenas converter um dicionário Python em JSON
ajustado em

@tunedconsulting Eu não tentei serializar uma instância do Datastore Model com json.dumps, mas presumo que funcionaria com qualquer objeto. Um relatório de bug deve ser submetido se não for como a documentação afirma que json.dumps leva um objeto como parâmetro. É adicionado como um comentário com apenas um comentário de que não existia em 2009. Esta resposta adicionada porque parece um pouco desatualizada, mas se não funcionar, terei todo o gosto em removê-la.
HMR

1
Se você tentar json.dumps um objeto de entidade ou uma classe de modelo, obterá TypeError: 'não é JSON serializável' <Object at 0x0xxxxxx>. O Datastore do GAE tem seus próprios tipos de dados (para datas, por exemplo). A resposta certa atual, testada e funcionando, é a do dmw que transforma alguns tipos de dados problemáticos em serializáveis.
sintonizado

@tunedconsulting Obrigado por sua opinião sobre isso, atualizarei minha resposta.
HMR
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.