TypeError: ObjectId ('') no es JSON serializable

109

Mi respuesta de MongoDB después de consultar una función agregada en un documento usando Python, devuelve una respuesta válida y puedo imprimirla pero no puedo devolverla.

Error:

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

Impresión:

{'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}

Pero cuando trato de regresar:

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

Es llamada 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

db está bien conectado y la colección también está allí y obtuve un resultado esperado válido, pero cuando intento devolverlo, me da un error de Json. Cualquier idea de cómo volver a convertir la respuesta en JSON. Gracias

Irfan
fuente

Respuestas:

118

Debes definir tu propiedad JSONEncodery usarla:

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)

También es posible usarlo de la siguiente manera.

json.encode(analytics, cls=JSONEncoder)
defuz
fuente
¡Perfecto! Funcionó para mí. Ya tengo una clase de codificador Json, ¿cómo puedo fusionar eso con la tuya? Mi clase de codificación Json ya es: '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, solo agregue if isinstance(o, ObjectId): return str(o)antes returnen el método default.
defuz
2
¿Podría agregar from bson import ObjectId, para que todos puedan copiar y pegar aún más rápido? ¡Gracias!
Liviu Chircu
@defuz ¿Por qué no usar str? ¿Qué hay de malo en ese enfoque?
Kevin
@defuz: cuando intento usar esto, ObjectID se elimina, pero mi respuesta json se divide en caracteres individuales. Quiero decir, cuando imprimo cada elemento del json resultante en un bucle for, obtengo cada carácter como un elemento. Alguna idea de como resolver esto?
Varij Kapil
119

Pymongo proporciona json_util ; puede usar ese en su lugar para manejar tipos BSON

tim
fuente
Estoy de acuerdo con @tim, esta es la forma correcta de lidiar con los datos BSON provenientes de mongo. api.mongodb.org/python/current/api/bson/json_util.html
Joshua Powell
Sí, parece ser más
fácil
En realidad, esa es la mejor manera.
Rahul
14
Un ejemplo aquí sería un poco más útil, ya que esta es la mejor manera, pero la documentación vinculada no es la más fácil de usar para los novatos
Jake
2
from bson import json_util json.loads(json_util.dumps(user_collection)) ^ esto funcionó después de instalar python-bsonjs conpipenv 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"}}]'

Ejemplo real de json_util .

A diferencia de jsonify de Flask, los "volcados" devolverán una cadena, por lo que no se puede usar como un reemplazo 1: 1 de jsonify de Flask.

Pero esta pregunta muestra que podemos serializar usando json_util.dumps (), volver a convertir a dict usando json.loads () y finalmente llamar a jsonify de Flask en él.

Ejemplo (derivado de la respuesta de la pregunta 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 solución convertirá ObjectId y otros (es decir, binario, código, etc.) en una cadena equivalente como "$ oid".

La salida JSON se vería así:

{
  "_id": {
    "$oid": "abc123"
  }
}
Garren S
fuente
Solo para aclarar, no es necesario llamar a 'jsonify' directamente desde un controlador de solicitudes Flask, solo devuelva el resultado desinfectado.
oferei
Estás absolutamente en lo correcto. Un dictado de Python (que devuelve json.loads) debería ser jsonificado automáticamente por Flask.
Garren S
¿No es un objeto dict no invocable?
SouvikMaji
@ rick112358 ¿cómo se relaciona un dictado que no se puede llamar con esta sesión de preguntas y respuestas?
Garren S
también puede usar json_util.loads () para recuperar exactamente el mismo diccionario (en lugar de uno con la tecla '$ 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 es el ejemplo de muestra para convertir BSON en un objeto JSON. Puedes probar esto.

vinit kantrod
fuente
21

La mayoría de los usuarios que reciben el error "no serializable JSON" simplemente necesitan especificar default=strcuándo usar json.dumps. Por ejemplo:

json.dumps(my_obj, default=str)

Esto forzará una conversión a str, evitando el error. Por supuesto, luego mire la salida generada para confirmar que es lo que necesita.

Acumenus
fuente
16

Como reemplazo rápido, puede cambiar {'owner': objectid}a {'owner': str(objectid)}.

Pero definir el tuyo propio JSONEncoderes una mejor solución, depende de tus requisitos.

MostafaR
fuente
6

Publicar aquí ya que creo que puede ser útil para las personas que usan Flaskcon pymongo. Esta es mi configuración actual de "mejores prácticas" para permitir que flask marshall pymongo bson tipos de datos.

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 qué hacer esto en lugar de servir BSON o JSON extendido mongod ?

Creo que servir JSON especial de mongo supone una carga para las aplicaciones cliente. A la mayoría de las aplicaciones cliente no les importará el uso de objetos mongo de forma compleja. Si sirvo json extendido, ahora tengo que usarlo del lado del servidor y del lado del cliente. ObjectIdy Timestampson más fáciles de trabajar como cadenas y esto mantiene toda esta locura de clasificación de mongo en cuarentena en el servidor.

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

Creo que trabajar con esto es menos oneroso para la mayoría de las aplicaciones que.

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

Así es como recientemente solucioné el error

    @app.route('/')
    def home():
        docs = []
        for doc in db.person.find():
            doc.pop('_id') 
            docs.append(doc)
        return jsonify(docs)
Jcc.Sanabria
fuente
en este caso, no está pasando el atributo '_id', sino que simplemente eliminó '_id' y pasó otros atributos del documento
Muhriddin Ismoilov
3

¡Sé que publico tarde, pero pensé que ayudaría al menos a algunas personas!

Tanto los ejemplos mencionados por tim como defuz (que son los más votados) funcionan perfectamente bien. Sin embargo, existe una pequeña diferencia que a veces puede ser significativa.

  1. El siguiente método agrega un campo adicional que es redundante y puede no ser ideal en todos los casos

Pymongo proporciona json_util; puede usar ese en su lugar para manejar tipos BSON

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

  1. Donde la clase JsonEncoder da la misma salida en el formato de cadena que necesitamos y necesitamos usar json.loads (salida) además. Pero conduce a

Salida: {"_id": "abc123"}

Aunque el primer método parece simple, ambos métodos requieren un esfuerzo mínimo.

rohithnama
fuente
esto es muy útil para el pytest-mongodbcomplemento al crear accesorios
tsveti_iko
3

en mi caso necesitaba algo como esto:

class JsonEncoder():
    def encode(self, o):
        if '_id' in o:
            o['_id'] = str(o['_id'])
        return o
Mahorad
fuente
1
+1 ¡Ja! ¿Podría haber sido más simple? 😍 En términos generales; para evitar toda la confusión con los codificadores personalizados y la importación de bson, envíe ObjectID a la cadena :object['_id'] = str(object['_id'])
Vexy
2

Jsonify de Flask proporciona una mejora de seguridad como se describe en Seguridad JSON . Si se usa un codificador personalizado con Flask, es mejor considerar los puntos discutidos en Seguridad JSON

Anish
fuente
2

Me gustaría proporcionar una solución adicional que mejore la respuesta aceptada. Anteriormente he proporcionado las respuestas en otro hilo aquí .

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

Si no va a necesitar el _id de los registros, recomendaré desarmarlo cuando consulte la base de datos, lo que le permitirá imprimir los registros devueltos directamente, por ejemplo.

Para desarmar el _id al consultar y luego imprimir datos en un bucle, escribe algo como esto

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

SOLUCIÓN para: mongoengine + marshmallow

Si usa mongoenginey marshamallow, esta solución podría ser aplicable para usted.

Básicamente, importé el Stringcampo de marshmallow y sobrescribí el valor predeterminado Schema idpara Stringcodificarlo.

from marshmallow import Schema
from marshmallow.fields import String

class FrontendUserSchema(Schema):

    id = String()

    class Meta:
        fields = ("id", "email")
Lukasz Dynowski
fuente
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
fuente
0

Si no desea una _idrespuesta, puede refactorizar su código de esta manera:

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

Esto eliminará el TypeError: ObjectId('') is not JSON serializableerror.

sarthakgupta072
fuente