TypeError: ObjectId ('') n'est pas sérialisable JSON

111

Ma réponse de MongoDB après avoir interrogé une fonction agrégée sur un document en utilisant Python, elle renvoie une réponse valide et je peux l'imprimer mais je ne peux pas la renvoyer.

Erreur:

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

Impression:

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

Mais quand j'essaye de revenir:

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

C'est l'appel 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 connecté et la collection est là aussi et j'ai récupéré un résultat attendu valide, mais lorsque j'essaye de retourner, cela me donne une erreur Json. Toute idée de la façon de reconvertir la réponse en JSON. Merci

Irfan
la source

Réponses:

120

Vous devez définir vous-même JSONEncoderet l'utiliser:

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)

Il est également possible de l'utiliser de la manière suivante.

json.encode(analytics, cls=JSONEncoder)
défuz
la source
Parfait! Cela a fonctionné pour moi. J'ai déjà une classe d'encodeur Json, comment puis-je la fusionner avec la vôtre? Ma classe d'encodage Json déjà est: '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, ajoutez juste if isinstance(o, ObjectId): return str(o)avant returndans la méthode default.
defuz
2
Pourriez-vous ajouter from bson import ObjectId, pour que tout le monde puisse copier-coller encore plus rapidement? Merci!
Liviu Chircu
@defuz Pourquoi ne pas simplement utiliser str? Quel est le problème avec cette approche?
Kevin
@defuz: Lorsque j'essaie d'utiliser ceci, ObjectID est supprimé, mais ma réponse json est divisée en caractères uniques. Je veux dire, lorsque j'imprime chaque élément du json résultant dans une boucle for, j'obtiens chaque caractère en tant qu'élément. Aucune idée sur la façon de résoudre ça?
Varij Kapil
120

Pymongo fournit json_util - vous pouvez utiliser celui-ci à la place pour gérer les types BSON

Tim
la source
Je suis d'accord avec @tim, c'est une manière correcte de traiter les données BSON provenant de mongo. api.mongodb.org/python/current/api/bson/json_util.html
Joshua Powell
Oui, cela semble être plus sans tracas si nous utilisons de cette façon
jonprasetyo
C'est la meilleure façon en fait.
Rahul
14
Un exemple ici serait un peu plus utile, car c'est le meilleur moyen, mais la documentation liée n'est pas la plus conviviale pour les noobs
Jake
2
from bson import json_util json.loads(json_util.dumps(user_collection)) ^ cela a fonctionné après l'installation de python-bsonjs avecpipenv 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"}}]'

Exemple réel de json_util .

Contrairement à jsonify de Flask, "dumps" renverra une chaîne, donc il ne peut pas être utilisé comme un remplacement 1: 1 du jsonify de Flask.

Mais cette question montre que nous pouvons sérialiser en utilisant json_util.dumps (), reconvertir en dict en utilisant json.loads () et enfin appeler le jsonify de Flask dessus.

Exemple (dérivé de la réponse à la question précédente):

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

Cette solution convertira ObjectId et d'autres (c'est-à-dire Binaire, Code, etc.) en une chaîne équivalente telle que "$ oid".

La sortie JSON ressemblerait à ceci:

{
  "_id": {
    "$oid": "abc123"
  }
}
Garren S
la source
Juste pour clarifier, pas besoin d'appeler 'jsonify' directement depuis un gestionnaire de requêtes Flask - il suffit de retourner le résultat nettoyé.
oferei
Vous avez absolument raison. Un dict Python (que json.loads retourne) doit être automatiquement jsonifié par Flask.
Garren S
Un objet dict n'est-il pas appelable?
SouvikMaji
@ rick112358 Comment un dict qui ne peut pas être appelé est-il lié à ce Q&A?
Garren S
vous pouvez également utiliser json_util.loads () pour récupérer exactement le même dictionnaire (au lieu d'un avec la clé '$ oid').
rGun
22

La plupart des utilisateurs qui reçoivent l'erreur "non sérialisable JSON" doivent simplement le spécifier default=strlors de l'utilisation json.dumps. Par exemple:

json.dumps(my_obj, default=str)

Cela forcera une conversion en str, empêchant l'erreur. Bien sûr, regardez ensuite la sortie générée pour confirmer que c'est ce dont vous avez besoin.

Acumenus
la source
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)

Voici l'exemple d'exemple de conversion de BSON en objet JSON. Vous pouvez essayer ceci.

vinit kantrod
la source
16

En remplacement rapide, vous pouvez passer {'owner': objectid}à {'owner': str(objectid)}.

Mais définir la vôtre JSONEncoderest une meilleure solution, cela dépend de vos besoins.

MostafaR
la source
6

Publier ici car je pense que cela peut être utile pour les personnes utilisant Flaskavec pymongo. C'est ma configuration actuelle de "meilleure pratique" pour permettre à flask de rassembler les types de données 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

Pourquoi faire cela au lieu de servir BSON ou mongod Extended JSON ?

Je pense que servir mongo special JSON met un fardeau sur les applications clientes. La plupart des applications clientes ne se soucieront pas d'utiliser des objets mongo de manière complexe. Si je sers du json étendu, je dois maintenant l'utiliser côté serveur et côté client. ObjectIdet Timestampsont plus faciles à travailler en tant que chaînes et cela maintient toute cette folie de marshalling mongo en quarantaine sur le serveur.

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

Je pense que cela est moins coûteux à utiliser pour la plupart des applications que.

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

Voici comment j'ai récemment corrigé l'erreur

    @app.route('/')
    def home():
        docs = []
        for doc in db.person.find():
            doc.pop('_id') 
            docs.append(doc)
        return jsonify(docs)
Jcc.Sanabrie
la source
dans ce cas, vous ne passez pas l'attribut '_id', mais supprimez simplement '_id' et
avez
3

Je sais que je poste en retard, mais j'ai pensé que cela aiderait au moins quelques personnes!

Les exemples mentionnés par tim et defuz (qui sont les mieux votés) fonctionnent parfaitement bien. Cependant, il existe une différence infime qui peut parfois être significative.

  1. La méthode suivante ajoute un champ supplémentaire qui est redondant et peut ne pas être idéal dans tous les cas

Pymongo fournit json_util - vous pouvez utiliser celui-ci à la place pour gérer les types BSON

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

  1. Où la classe JsonEncoder donne la même sortie au format de chaîne que nous avons besoin et nous devons utiliser json.loads (sortie) en plus. Mais cela conduit à

Sortie: {"_id": "abc123"}

Même si la première méthode semble simple, les deux méthodes nécessitent un effort très minime.

Rohithnama
la source
ceci est très utile pour le pytest-mongodbplugin lors de la création de luminaires
tsveti_iko
3

dans mon cas, j'avais besoin de quelque chose comme ça:

class JsonEncoder():
    def encode(self, o):
        if '_id' in o:
            o['_id'] = str(o['_id'])
        return o
Mahorade
la source
1
+1 Ha! Cela aurait-il pu être plus simple 😍 D'une manière générale; pour éviter tout le fuzz avec les encodeurs personnalisés et l'importation bson, transtypez ObjectID en chaîne :object['_id'] = str(object['_id'])
Vexy le
2

Jsonify de Flask fournit une amélioration de la sécurité comme décrit dans la sécurité JSON . Si un encodeur personnalisé est utilisé avec Flask, il est préférable de prendre en compte les points abordés dans la sécurité JSON

Anish
la source
2

Je souhaite apporter une solution supplémentaire qui améliore la réponse acceptée. J'ai déjà fourni les réponses dans un autre fil ici .

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
la source
1

Si vous n'avez pas besoin du _id des enregistrements, je vous recommanderai de le désactiver lors de l'interrogation de la base de données, ce qui vous permettra d'imprimer directement les enregistrements retournés, par exemple

Pour annuler le _id lors de l'interrogation, puis imprimer des données dans une boucle, vous écrivez quelque chose comme ceci

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

SOLUTION pour: mongoengine + guimauve

Si vous utilisez mongoengineetmarshamallow alors cette solution peut être applicable pour vous.

Fondamentalement, j'ai importé un Stringchamp de guimauve et j'ai écrasé la valeur par défaut Schema idpour être Stringencodé.

from marshmallow import Schema
from marshmallow.fields import String

class FrontendUserSchema(Schema):

    id = String()

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

Si vous ne voulez pas _idde réponse, vous pouvez refactoriser votre code comme ceci:

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

Cela supprimera l' TypeError: ObjectId('') is not JSON serializableerreur.

sarthakgupta072
la source