TypeError: ObjectId ('') ist nicht JSON-serialisierbar

109

Meine Antwort von MongoDB nach Abfrage einer aggregierten Funktion für ein Dokument mit Python. Es gibt eine gültige Antwort zurück und ich kann sie drucken, aber nicht zurückgeben.

Error:

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

Drucken:

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

Aber wenn ich versuche zurückzukehren:

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

Es ist RESTfull Aufruf:

@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 ist gut verbunden und die Sammlung ist auch da und ich habe ein gültiges erwartetes Ergebnis zurückbekommen, aber wenn ich versuche, es zurückzugeben, gibt es mir einen Json-Fehler. Jede Idee, wie die Antwort wieder in JSON konvertiert werden kann. Vielen Dank

Irfan
quelle

Antworten:

118

Sie sollten Ihre eigenen definieren JSONEncoderund verwenden:

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)

Es ist auch möglich, es auf folgende Weise zu verwenden.

json.encode(analytics, cls=JSONEncoder)
defuz
quelle
Perfekt! Es hat bei mir funktioniert. Ich habe bereits eine Json-Encoder-Klasse. Wie kann ich das mit Ihrer Klasse zusammenführen? Meine bereits vorhandene Json-Codierungsklasse lautet: '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, einfach if isinstance(o, ObjectId): return str(o)vorher returnin der Methode hinzufügen default.
defuz
2
Könnten Sie hinzufügen from bson import ObjectId, damit jeder noch schneller kopieren und einfügen kann? Vielen Dank!
Liviu Chircu
@defuz Warum nicht einfach benutzen str? Was ist falsch an diesem Ansatz?
Kevin
@defuz: Wenn ich versuche, dies zu verwenden, wird die ObjectID entfernt, aber meine JSON-Antwort wird in einzelne Zeichen unterteilt. Ich meine, wenn ich jedes Element aus dem resultierenden json in einer for-Schleife drucke, bekomme ich jedes Zeichen als Element. Irgendeine Idee, wie man das löst?
Varij Kapil
119

Pymongo bietet json_util - Sie können dieses stattdessen verwenden, um BSON-Typen zu verarbeiten

tim
quelle
Ich stimme @tim zu, dies ist der richtige Weg, um mit BSON-Daten von Mongo umzugehen. api.mongodb.org/python/current/api/bson/json_util.html
Joshua Powell
Ja, scheint eher
stressfrei
Das ist eigentlich der beste Weg.
Rahul
14
Ein Beispiel hier wäre etwas hilfreicher, da dies der beste Weg ist, aber die verknüpfte Dokumentation für Noobs nicht besonders benutzerfreundlich ist
Jake
2
from bson import json_util json.loads(json_util.dumps(user_collection)) ^ Dies funktionierte nach der Installation von Python-Bsonjs mitpipenv 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"}}]'

Aktuelles Beispiel von json_util .

Im Gegensatz zu Flask's jsonify geben "dumps" eine Zeichenfolge zurück, sodass sie nicht als 1: 1-Ersatz für Flask's jsonify verwendet werden können.

Aber diese Frage zeigt , dass wir serialisiert werden können json_util.dumps () konvertieren zurück zu DIKT mit json.loads () und rufen schließlich Flask des jsonify darauf.

Beispiel (abgeleitet aus der Antwort der vorherigen Frage):

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

Diese Lösung konvertiert ObjectId und andere (z. B. Binär, Code usw.) in ein Zeichenfolgenäquivalent wie "$ oid".

Die JSON-Ausgabe würde folgendermaßen aussehen:

{
  "_id": {
    "$oid": "abc123"
  }
}
Garren S.
quelle
Zur Verdeutlichung müssen Sie "jsonify" nicht direkt von einem Flask-Anforderungshandler aus aufrufen. Geben Sie einfach das bereinigte Ergebnis zurück.
oferei
Du hast absolut recht. Ein Python-Diktat (das von json.loads zurückgegeben wird) sollte automatisch von Flask jsonifiziert werden.
Garren S
Ist ein Diktierobjekt nicht nicht aufrufbar?
SouvikMaji
@ rick112358 Wie hängt ein nicht abrufbares Diktat mit diesen Fragen und Antworten zusammen?
Garren S
Sie können auch json_util.loads () verwenden, um genau dasselbe Wörterbuch zurückzugewinnen (anstelle eines mit dem Schlüssel '$ 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)

Dies ist das Beispielbeispiel für die Konvertierung von BSON in ein JSON-Objekt. Sie können dies versuchen.

vinit kantrod
quelle
21

Die meisten Benutzer, die den Fehler "Nicht JSON serialisierbar" erhalten, müssen default=strbei der Verwendung lediglich angeben json.dumps. Beispielsweise:

json.dumps(my_obj, default=str)

Dadurch wird eine Konvertierung erzwungen str, um den Fehler zu vermeiden. Schauen Sie sich dann natürlich die generierte Ausgabe an, um zu bestätigen, dass es das ist, was Sie brauchen.

Scharfsinn
quelle
16

Als schnellen Austausch, können Sie ändern {'owner': objectid}zu {'owner': str(objectid)}.

Die Definition Ihrer eigenen JSONEncoderLösung ist jedoch eine bessere Lösung. Sie hängt von Ihren Anforderungen ab.

MostafaR
quelle
6

Hier posten, da ich denke, dass es für Leute nützlich sein kann, die Flaskmit arbeiten pymongo. Dies ist meine aktuelle "Best Practice" -Einstellung, um zu ermöglichen, dass der Kolben Pymongo Bson-Datentypen marshallt.

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

Warum tun Sie dies, anstatt BSON oder Mongod Extended JSON zu bedienen ?

Ich denke, dass das Bereitstellen von Mongo-Spezial-JSON die Clientanwendungen belastet. Die meisten Client-Apps kümmern sich nicht darum, Mongo-Objekte auf komplexe Weise zu verwenden. Wenn ich Extended JSON bediene, muss ich es jetzt serverseitig und clientseitig verwenden. ObjectIdund Timestampsind einfacher als Strings zu arbeiten und dies hält all diesen Mongo-Marshalling-Wahnsinn auf dem Server unter Quarantäne.

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

Ich denke, dass es für die meisten Anwendungen weniger lästig ist, damit zu arbeiten als.

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

So habe ich kürzlich den Fehler behoben

    @app.route('/')
    def home():
        docs = []
        for doc in db.person.find():
            doc.pop('_id') 
            docs.append(doc)
        return jsonify(docs)
Jcc.Sanabria
quelle
In diesem Fall übergeben Sie nicht das Attribut '_id', sondern löschen einfach '_id' und übergeben andere Attribute von doc
Muhriddin Ismoilov
3

Ich weiß, dass ich zu spät poste, dachte aber, es würde zumindest ein paar Leuten helfen!

Beide Beispiele von tim und defuz (die am besten gewählt wurden) funktionieren einwandfrei. Es gibt jedoch einen winzigen Unterschied, der manchmal erheblich sein kann.

  1. Die folgende Methode fügt ein zusätzliches Feld hinzu, das redundant ist und möglicherweise nicht in allen Fällen ideal ist

Pymongo bietet json_util - Sie können dieses stattdessen verwenden, um BSON-Typen zu verarbeiten

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

  1. Wobei die JsonEncoder-Klasse die gleiche Ausgabe im Zeichenfolgenformat liefert, die wir benötigen, und wir zusätzlich json.loads (Ausgabe) verwenden müssen. Aber es führt zu

Ausgabe: {"_id": "abc123"}

Obwohl die erste Methode einfach aussieht, erfordern beide Methoden nur einen minimalen Aufwand.

Rohithnama
quelle
Dies ist sehr nützlich für das pytest-mongodbPlugin beim Erstellen von Fixtures
tsveti_iko
3

in meinem Fall brauchte ich so etwas:

class JsonEncoder():
    def encode(self, o):
        if '_id' in o:
            o['_id'] = str(o['_id'])
        return o
Mahorad
quelle
1
+1 Ha! Könnte es einfacher sein? 😍 Im Allgemeinen; Um den Fuzz mit benutzerdefinierten Encodern und dem Import von Bson zu vermeiden, setzen Sie ObjectID in einen String um :object['_id'] = str(object['_id'])
Vexy
2

Ich möchte eine zusätzliche Lösung anbieten, die die akzeptierte Antwort verbessert. Ich habe die Antworten zuvor in einem anderen Thread hier bereitgestellt .

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

Wenn Sie die _id der Datensätze nicht benötigen, empfehle ich, sie bei der Abfrage der Datenbank zu deaktivieren, damit Sie die zurückgegebenen Datensätze direkt drucken können, z

Um die _id beim Abfragen zu deaktivieren und dann Daten in einer Schleife zu drucken, schreiben Sie so etwas

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

LÖSUNG für: Mongoengine + Marshmallow

Wenn Sie mongoengineund verwendenmarshamallow dann verwenden, ist diese Lösung möglicherweise für Sie anwendbar.

Grundsätzlich habe ich ein StringFeld aus Marshmallow importiert und Schema iddie zu Stringcodierende Standardeinstellung überschrieben .

from marshmallow import Schema
from marshmallow.fields import String

class FrontendUserSchema(Schema):

    id = String()

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

Wenn Sie keine _idAntwort wünschen , können Sie Ihren Code folgendermaßen umgestalten:

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

Dadurch wird der TypeError: ObjectId('') is not JSON serializableFehler behoben.

sarthakgupta072
quelle