TypeError: ObjectId ('') non è serializzabile in JSON


109

La mia risposta da MongoDB dopo aver interrogato una funzione aggregata sul documento utilizzando Python, restituisce una risposta valida e posso stamparla ma non posso restituirla.

Errore:

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

Stampa:

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

Ma quando provo a tornare:

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

È una chiamata 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 è ben collegato e anche la raccolta è lì e ho ricevuto un risultato atteso valido ma quando provo a restituirlo mi dà un errore Json. Qualche idea su come riconvertire la risposta in JSON. Grazie

Risposte:


118

Dovresti definire il tuo proprietario JSONEncodere usarlo:

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)

È anche possibile utilizzarlo nel modo seguente.

json.encode(analytics, cls=JSONEncoder)

Perfetto! Ha funzionato per me. Ho già una classe di codifica Json, come posso unirla alla tua classe? La mia classe di codifica già Json è: '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, aggiungi solo if isinstance(o, ObjectId): return str(o)prima returnnel metodo default.
defuz

2
Potresti aggiungere from bson import ObjectId, in modo che tutti possano copiare e incollare ancora più velocemente? Grazie!
Liviu Chircu

@defuz Perché non basta usare str? Cosa c'è di sbagliato in questo approccio?
Kevin

@defuz: Quando provo a usarlo, ObjectID viene rimosso, ma la mia risposta json viene suddivisa in singoli caratteri. Voglio dire, quando stampo ogni elemento dal json risultante in un ciclo for ottengo ogni carattere come elemento. Qualche idea su come risolverlo?
Varij Kapil

119

Pymongo fornisce json_util : puoi usarlo invece per gestire i tipi BSON


Sono d'accordo con @tim, questo è il modo corretto di gestire i dati BSON provenienti da mongo. api.mongodb.org/python/current/api/bson/json_util.html
Joshua Powell,

Sì, sembra essere più un problema se usiamo in questo modo
jonprasetyo

In realtà è il modo migliore.
Rahul

14
Un esempio qui sarebbe un po 'più utile, poiché questo è il modo migliore, ma la documentazione collegata non è la più facile da usare per i niubbi
Jake

2
from bson import json_util json.loads(json_util.dumps(user_collection)) ^ questo ha funzionato dopo aver installato 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"}}]'

Esempio reale da json_util .

A differenza di jsonify di Flask, "dumps" restituirà una stringa, quindi non può essere utilizzato come sostituzione 1: 1 di jsonify di Flask.

Ma questa domanda mostra che possiamo serializzare usando json_util.dumps (), riconvertire a dict usando json.loads () e infine chiamare jsonify di Flask su di esso.

Esempio (derivato dalla risposta della domanda precedente):

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

Questa soluzione convertirà ObjectId e altri (es. Binario, codice, ecc.) In una stringa equivalente come "$ oid".

L'output JSON sarebbe simile a questo:

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

Giusto per chiarire, non è necessario chiamare "jsonify" direttamente da un gestore di richieste Flask: è sufficiente restituire il risultato disinfettato.
oferei

Hai assolutamente ragione. Un dict Python (restituito da json.loads) dovrebbe essere automaticamente jsonificato da Flask.
Garren S

Un oggetto dict non è richiamabile?
SouvikMaji

@ rick112358 in che modo un dict non richiamabile è correlato a questa domanda e risposta?
Garren S

puoi anche usare json_util.loads () per recuperare lo stesso identico dizionario (invece di uno con il tasto '$ 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)

Questo è l'esempio di esempio per convertire BSON in un oggetto JSON. Puoi provare questo.


21

La maggior parte degli utenti che ricevono l'errore "serializzabile non JSON" devono semplicemente specificare default=strdurante l'utilizzo json.dumps. Per esempio:

json.dumps(my_obj, default=str)

Ciò forzerà una conversione a str, prevenendo l'errore. Ovviamente poi guarda l'output generato per confermare che è quello che ti serve.


16

In sostituzione rapida, puoi passare {'owner': objectid}a {'owner': str(objectid)}.

Ma definire il tuo JSONEncoderè una soluzione migliore, dipende dalle tue esigenze.


6

Pubblicare qui perché penso che possa essere utile per le persone che usano Flaskcon pymongo. Questa è la mia attuale configurazione "best practice" per consentire a flask di eseguire il marshalling dei tipi di dati pymongo bson.

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

Perché farlo invece di servire BSON o JSON esteso mongod ?

Penso che servire lo speciale JSON di mongo metta un peso sulle applicazioni client. La maggior parte delle app client non si preoccuperà di utilizzare oggetti mongo in modo complesso. Se servo json esteso, ora devo usarlo lato server e lato client. ObjectIde Timestampsono più facili da lavorare come stringhe e questo mantiene tutta questa follia di smistamento di mongo in quarantena sul server.

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

Penso che questo sia meno oneroso con cui lavorare per la maggior parte delle applicazioni rispetto a.

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

4

Ecco come ho recentemente corretto l'errore

    @app.route('/')
    def home():
        docs = []
        for doc in db.person.find():
            doc.pop('_id') 
            docs.append(doc)
        return jsonify(docs)

in questo caso non stai passando l'attributo '_id', ma hai semplicemente cancellato '_id' e passato altri attributi di doc
Muhriddin Ismoilov

3

So che sto postando in ritardo ma ho pensato che avrebbe aiutato almeno alcune persone!

Entrambi gli esempi citati da tim e defuz (che sono i più votati) funzionano perfettamente. Tuttavia, c'è una piccola differenza che a volte potrebbe essere significativa.

  1. Il metodo seguente aggiunge un campo extra che è ridondante e potrebbe non essere l'ideale in tutti i casi

Pymongo fornisce json_util: puoi usarlo invece per gestire i tipi BSON

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

  1. Dove la classe JsonEncoder fornisce lo stesso output nel formato stringa di cui abbiamo bisogno e dobbiamo usare json.loads (output) in aggiunta. Ma porta a

Risultato: {"_id": "abc123"}

Anche se il primo metodo sembra semplice, entrambi richiedono uno sforzo minimo.


questo è molto utile per il pytest-mongodbplugin durante la creazione di dispositivi
tsveti_iko

3

nel mio caso avevo bisogno di qualcosa del genere:

class JsonEncoder():
    def encode(self, o):
        if '_id' in o:
            o['_id'] = str(o['_id'])
        return o

1
+1 Ha! Potrebbe essere stato più semplice 😍 In generale; per evitare tutto il fuzz con codificatori personalizzati e importazione bson, lancia ObjectID alla stringa :object['_id'] = str(object['_id'])
Vexy

2

Jsonify di Flask fornisce miglioramenti della sicurezza come descritto in JSON Security . Se viene utilizzato un codificatore personalizzato con Flask, è meglio considerare i punti discussi in JSON Security


2

Vorrei fornire una soluzione aggiuntiva che migliori la risposta accettata. In precedenza ho fornito le risposte in un altro thread qui .

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

1

Se non avrai bisogno dell'_id dei record, ti consiglio di disattivarlo quando esegui una query sul DB che ti consentirà di stampare direttamente i record restituiti, ad es.

Per annullare l'impostazione _id durante la query e quindi stampare i dati in un ciclo, scrivi qualcosa di simile

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

0

SOLUZIONE per: mongoengine + marshmallow

Se usi mongoenginee marshamallowallora questa soluzione potrebbe essere applicabile per te.

Fondamentalmente, ho importato il Stringcampo da marshmallow e ho sovrascritto il valore predefinito Schema idper essere Stringcodificato.

from marshmallow import Schema
from marshmallow.fields import String

class FrontendUserSchema(Schema):

    id = String()

    class Meta:
        fields = ("id", "email")

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

0

Se non vuoi una _idrisposta, puoi rifattorizzare il tuo codice in questo modo:

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

Questo rimuoverà l' TypeError: ObjectId('') is not JSON serializableerrore.

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.