TypeError: ObjectId ('') não é serializável em JSON

109

Minha resposta de volta do MongoDB depois de consultar uma função agregada no documento usando Python, ela retorna uma resposta válida e posso imprimi-la, mas não posso retorná-la.

Erro:

TypeError: ObjectId('51948e86c25f4b1d1c0d303c') is not JSON serializable

Impressão:

{'result': [{'_id': ObjectId('51948e86c25f4b1d1c0d303c'), 'api_calls_with_key': 4, 'api_calls_per_day': 0.375, 'api_calls_total': 6, 'api_calls_without_key': 2}], 'ok': 1.0}

Mas quando tento voltar:

TypeError: ObjectId('51948e86c25f4b1d1c0d303c') is not JSON serializable

É uma chamada RESTfull:

@appv1.route('/v1/analytics')
def get_api_analytics():
    # get handle to collections in MongoDB
    statistics = sldb.statistics

    objectid = ObjectId("51948e86c25f4b1d1c0d303c")

    analytics = statistics.aggregate([
    {'$match': {'owner': objectid}},
    {'$project': {'owner': "$owner",
    'api_calls_with_key': {'$cond': [{'$eq': ["$apikey", None]}, 0, 1]},
    'api_calls_without_key': {'$cond': [{'$ne': ["$apikey", None]}, 0, 1]}
    }},
    {'$group': {'_id': "$owner",
    'api_calls_with_key': {'$sum': "$api_calls_with_key"},
    'api_calls_without_key': {'$sum': "$api_calls_without_key"}
    }},
    {'$project': {'api_calls_with_key': "$api_calls_with_key",
    'api_calls_without_key': "$api_calls_without_key",
    'api_calls_total': {'$add': ["$api_calls_with_key", "$api_calls_without_key"]},
    'api_calls_per_day': {'$divide': [{'$add': ["$api_calls_with_key", "$api_calls_without_key"]}, {'$dayOfMonth': datetime.now()}]},
    }}
    ])


    print(analytics)

    return analytics

O banco de dados está bem conectado e a coleção também está lá e recebi um resultado esperado válido, mas quando tento retornar, ele me dá um erro de Json. Alguma ideia de como converter a resposta de volta em JSON. obrigado

Irfan
fonte

Respostas:

118

Você deve definir seu próprio JSONEncodere usá-lo:

import json
from bson import ObjectId

class JSONEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, ObjectId):
            return str(o)
        return json.JSONEncoder.default(self, o)

JSONEncoder().encode(analytics)

Também é possível usá-lo da seguinte maneira.

json.encode(analytics, cls=JSONEncoder)
defuz
fonte
Perfeito! Funcionou para mim Já tenho uma classe de codificador Json, como posso mesclar isso com a sua classe? Minha classe de codificação Json já é: 'class MyJsonEncoder (json.JSONEncoder): def default (self, obj): if isinstance (obj, datetime): return str (obj.strftime ("% Y-% m-% d% H:% M:% S")) return json.JSONEncoder.default (self, obj) '
Irfan
1
@IrfanDayan, basta adicionar if isinstance(o, ObjectId): return str(o)antes returnno método default.
defuz
2
Você poderia adicionar from bson import ObjectId, para que todos possam copiar e colar ainda mais rápido? Obrigado!
Liviu Chircu
@defuz Por que não basta usar str? O que há de errado com essa abordagem?
Kevin
@defuz: Quando tento usar isso, ObjectID é removido, mas minha resposta json é dividida em caracteres únicos. Quero dizer, quando imprimo cada elemento do json resultante em um loop for, recebo cada caractere como um elemento. Alguma idéia de como resolver isso?
Varij Kapil
119

Pymongo fornece json_util - você pode usar esse em vez de lidar com tipos BSON

tim
fonte
Eu concordo com @tim, esta é a maneira correta de lidar com dados BSON vindos do mongo. api.mongodb.org/python/current/api/bson/json_util.html
Joshua Powell
Sim, parece ser mais fácil se usarmos desta forma
jonprasetyo
Essa é a melhor maneira, na verdade.
Rahul
14
Um exemplo aqui seria um pouco mais útil, pois esta é a melhor maneira, mas a documentação vinculada não é a mais amigável para iniciantes
Jake
2
from bson import json_util json.loads(json_util.dumps(user_collection)) ^ funcionou depois de instalar o python-bsonjs compipenv install python-bsonjs
NBhat
38
>>> from bson import Binary, Code
>>> from bson.json_util import dumps
>>> dumps([{'foo': [1, 2]},
...        {'bar': {'hello': 'world'}},
...        {'code': Code("function x() { return 1; }")},
...        {'bin': Binary("")}])
'[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }", "$scope": {}}}, {"bin": {"$binary": "AQIDBA==", "$type": "00"}}]'

Exemplo real de json_util .

Ao contrário do jsonify do Flask, "dumps" retornará uma string, portanto, não pode ser usado como uma substituição 1: 1 do jsonify do Flask.

Mas essa pergunta mostra que podemos serializar usando json_util.dumps (), converter de volta para dict usando json.loads () e finalmente chamar o jsonify do Flask nele.

Exemplo (derivado da resposta da pergunta anterior):

from bson import json_util, ObjectId
import json

#Lets create some dummy document to prove it will work
page = {'foo': ObjectId(), 'bar': [ObjectId(), ObjectId()]}

#Dump loaded BSON to valid JSON string and reload it as dict
page_sanitized = json.loads(json_util.dumps(page))
return page_sanitized

Esta solução irá converter ObjectId e outros (ou seja, Binário, Código, etc) em uma string equivalente, como "$ oid".

A saída JSON ficaria assim:

{
  "_id": {
    "$oid": "abc123"
  }
}
Garren S
fonte
Só para esclarecer, não há necessidade de chamar 'jsonify' diretamente de um manipulador de solicitação Flask - basta retornar o resultado limpo.
oferei
Você está absolutamente correto. Um dicionário Python (que retorna json.loads) deve ser automaticamente jsonificado pelo Flask.
Garren S
Um objeto dict não pode ser chamado?
SouvikMaji
@ rick112358 como um dict que não pode ser chamado se relaciona a este Q&A?
Garren S
você também pode usar json_util.loads () para obter exatamente o mesmo dicionário de volta (em vez de um com a chave '$ oid').
rGun
21
from bson import json_util
import json

@app.route('/')
def index():
    for _ in "collection_name".find():
        return json.dumps(i, indent=4, default=json_util.default)

Este é o exemplo de amostra para converter BSON em objeto JSON. Você pode tentar isso.

vinit kantrod
fonte
21

A maioria dos usuários que recebe o erro "não serializável em JSON" simplesmente precisa especificar default=strao usar json.dumps. Por exemplo:

json.dumps(my_obj, default=str)

Isso forçará uma conversão para str, evitando o erro. Obviamente, observe a saída gerada para confirmar se é o que você precisa.

Acumenus
fonte
16

Como uma substituição rápida, você pode mudar {'owner': objectid}para {'owner': str(objectid)}.

Mas definir o seu próprio JSONEncoderé uma solução melhor, depende dos seus requisitos.

MostafaR
fonte
6

Postando aqui, pois acho que pode ser útil para pessoas que usam Flaskcom pymongo. Esta é minha configuração de "prática recomendada" atual para permitir que o flask marque os tipos de dados bson pymongo.

mongoflask.py

from datetime import datetime, date

import isodate as iso
from bson import ObjectId
from flask.json import JSONEncoder
from werkzeug.routing import BaseConverter


class MongoJSONEncoder(JSONEncoder):
    def default(self, o):
        if isinstance(o, (datetime, date)):
            return iso.datetime_isoformat(o)
        if isinstance(o, ObjectId):
            return str(o)
        else:
            return super().default(o)


class ObjectIdConverter(BaseConverter):
    def to_python(self, value):
        return ObjectId(value)

    def to_url(self, value):
        return str(value)

app.py

from .mongoflask import MongoJSONEncoder, ObjectIdConverter

def create_app():
    app = Flask(__name__)
    app.json_encoder = MongoJSONEncoder
    app.url_map.converters['objectid'] = ObjectIdConverter

    # Client sends their string, we interpret it as an ObjectId
    @app.route('/users/<objectid:user_id>')
    def show_user(user_id):
        # setup not shown, pretend this gets us a pymongo db object
        db = get_db()

        # user_id is a bson.ObjectId ready to use with pymongo!
        result = db.users.find_one({'_id': user_id})

        # And jsonify returns normal looking json!
        # {"_id": "5b6b6959828619572d48a9da",
        #  "name": "Will",
        #  "birthday": "1990-03-17T00:00:00Z"}
        return jsonify(result)


    return app

Por que fazer isso em vez de servir BSON ou JSON estendido mongod ?

Acho que servir JSON especial ao mongo sobrecarrega os aplicativos clientes. A maioria dos aplicativos cliente não se importará com o uso de objetos mongo de forma complexa. Se eu servir o json estendido, agora tenho que usá-lo no lado do servidor e no lado do cliente. ObjectIde Timestampsão mais fáceis de trabalhar como strings e isso mantém toda essa loucura de marshalling mongo em quarentena para o servidor.

{
  "_id": "5b6b6959828619572d48a9da",
  "created_at": "2018-08-08T22:06:17Z"
}

Acho que isso é menos oneroso de se trabalhar para a maioria dos aplicativos do que.

{
  "_id": {"$oid": "5b6b6959828619572d48a9da"},
  "created_at": {"$date": 1533837843000}
}
Nackjicholson
fonte
4

Foi assim que corrigi o erro recentemente

    @app.route('/')
    def home():
        docs = []
        for doc in db.person.find():
            doc.pop('_id') 
            docs.append(doc)
        return jsonify(docs)
Jcc.Sanabria
fonte
neste caso, você não está passando o atributo '_id', em vez disso, apenas excluiu '_id' e passou outros atributos do doc
Muhriddin Ismoilov
3

Sei que estou postando tarde, mas pensei que ajudaria pelo menos algumas pessoas!

Ambos os exemplos mencionados por tim e defuz (que são os mais votados) funcionam perfeitamente bem. No entanto, há uma diferença mínima que às vezes pode ser significativa.

  1. O método a seguir adiciona um campo extra que é redundante e pode não ser ideal em todos os casos

Pymongo fornece json_util - você pode usar esse em vez de lidar com tipos BSON

Resultado: {"_id": {"$ oid": "abc123"}}

  1. Onde, como a classe JsonEncoder fornece a mesma saída no formato de string que precisamos e precisamos usar json.loads (saída) adicionalmente. Mas isso leva a

Resultado: {"_id": "abc123"}

Mesmo assim, o primeiro método parece simples, ambos os métodos precisam de um esforço mínimo.

rohithnama
fonte
isso é muito útil para o pytest-mongodbplugin ao criar fixtures
tsveti_iko
3

no meu caso, eu precisava de algo assim:

class JsonEncoder():
    def encode(self, o):
        if '_id' in o:
            o['_id'] = str(o['_id'])
        return o
Mahorad
fonte
1
+1 Ha! Poderia ter sido mais simples 😍 De um modo geral; para evitar todo o fuzz com codificadores personalizados e importação de bson, lance ObjectID para string :object['_id'] = str(object['_id'])
Vexy,
2

O jsonify do Flask fornece aprimoramento de segurança conforme descrito em Segurança JSON . Se um codificador personalizado for usado com o Flask, é melhor considerar os pontos discutidos no JSON Security

Anish
fonte
2

Eu gostaria de fornecer uma solução adicional que melhora a resposta aceita. Eu já forneci as respostas em outro tópico aqui .

from flask import Flask
from flask.json import JSONEncoder

from bson import json_util

from . import resources

# define a custom encoder point to the json_util provided by pymongo (or its dependency bson)
class CustomJSONEncoder(JSONEncoder):
    def default(self, obj): return json_util.default(obj)

application = Flask(__name__)
application.json_encoder = CustomJSONEncoder

if __name__ == "__main__":
    application.run()
aitorhh
fonte
1

Se você não precisar do _id dos registros, recomendo removê-lo ao consultar o banco de dados, o que permitirá que você imprima os registros retornados diretamente, por exemplo

Para remover o _id ao consultar e depois imprimir os dados em um loop, você deve escrever algo assim

records = mycollection.find(query, {'_id': 0}) #second argument {'_id':0} unsets the id from the query
for record in records:
    print(record)
Ibrahim Isa
fonte
0

SOLUÇÃO para: mongoengine + marshmallow

Se você usar mongoenginee, marshamallowentão, esta solução pode ser aplicável para você.

Basicamente, importei o Stringcampo do marshmallow e substituí o padrão Schema idpara ser Stringcodificado.

from marshmallow import Schema
from marshmallow.fields import String

class FrontendUserSchema(Schema):

    id = String()

    class Meta:
        fields = ("id", "email")
Lukasz Dynowski
fonte
0
from bson.objectid import ObjectId
from core.services.db_connection import DbConnectionService

class DbExecutionService:
     def __init__(self):
        self.db = DbConnectionService()

     def list(self, collection, search):
        session = self.db.create_connection(collection)
        return list(map(lambda row: {i: str(row[i]) if isinstance(row[i], ObjectId) else row[i] for i in row}, session.find(search))
Ana paula lopes
fonte
0

Se você não quiser uma _idresposta, pode refatorar seu código mais ou menos assim:

jsonResponse = getResponse(mock_data)
del jsonResponse['_id'] # removes '_id' from the final response
return jsonResponse

Isso removerá o TypeError: ObjectId('') is not JSON serializableerro.

sarthakgupta072
fonte