TypeError: ObjectId ('') nie można serializować w formacie JSON

111

Moja odpowiedź z MongoDB po zapytaniu o zagregowaną funkcję w dokumencie za pomocą Pythona, zwraca prawidłową odpowiedź i mogę ją wydrukować, ale nie mogę jej zwrócić.

Błąd:

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

Wydrukować:

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

Ale kiedy próbuję wrócić:

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

To jest RESTfull call:

@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 jest dobrze połączony, a kolekcja też tam jest i wróciłem prawidłowy oczekiwany wynik, ale kiedy próbuję zwrócić, wyświetla mi się błąd Json. Każdy pomysł, jak przekonwertować odpowiedź z powrotem na JSON. Dzięki

Irfan
źródło

Odpowiedzi:

120

Powinieneś zdefiniować swój własny JSONEncoderi używać go:

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)

Możliwe jest również użycie go w następujący sposób.

json.encode(analytics, cls=JSONEncoder)
defuz
źródło
Idealny! U mnie to zadziałało. Mam już klasę kodera Json, jak mogę to scalić z twoją klasą? Moja już klasa kodowania Json to: '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, po prostu dodaj if isinstance(o, ObjectId): return str(o)przed returnmetodą default.
defuz
2
Czy możesz dodać from bson import ObjectId, aby każdy mógł jeszcze szybciej kopiować i wklejać? Dzięki!
Liviu Chircu
@defuz Dlaczego nie po prostu użyć str? Co jest złego w tym podejściu?
Kevin
@defuz: Kiedy próbuję tego użyć, ObjectID jest usuwany, ale moja odpowiedź json jest dzielona na pojedyncze znaki. Chodzi mi o to, że kiedy drukuję każdy element z wynikowego pliku JSON w pętli for, otrzymuję każdy znak jako element. Masz jakiś pomysł, jak to rozwiązać?
Varij Kapil
120

Pymongo zapewnia json_util - możesz użyć tego zamiast obsługi typów BSON

tim
źródło
Zgadzam się z @tim, jest to poprawny sposób radzenia sobie z danymi BSON pochodzącymi z mongo. api.mongodb.org/python/current/api/bson/json_util.html
Joshua Powell
Tak, wydaje się to być bardziej bezproblemowe, jeśli używamy w ten sposób
jonprasetyo
Właściwie to najlepszy sposób.
Rahul
14
Przykład tutaj byłby trochę bardziej pomocny, ponieważ jest to najlepszy sposób, ale dołączona dokumentacja nie jest najbardziej przyjazna dla użytkownika dla noobów
Jake
2
from bson import json_util json.loads(json_util.dumps(user_collection)) ^ to zadziałało po zainstalowaniu python-bsonjs zpipenv 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"}}]'

Rzeczywisty przykład z json_util .

W przeciwieństwie do jsonify Flaska, „dumps” zwróci ciąg, więc nie może być używany jako zamiennik 1: 1 jsonify Flask.

Ale to pytanie pokazuje, że możemy serializować za pomocą json_util.dumps (), przekonwertować z powrotem na dict za pomocą json.loads () i ostatecznie wywołać na nim jsonify Flaska.

Przykład (zaczerpnięty z odpowiedzi na poprzednie pytanie):

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

To rozwiązanie konwertuje ObjectId i inne (np. Binary, Code itp.) Na ciąg znaków równoważny, taki jak „$ oid”.

Dane wyjściowe JSON wyglądałyby następująco:

{
  "_id": {
    "$oid": "abc123"
  }
}
Garren S
źródło
Aby wyjaśnić, nie ma potrzeby wywoływania „jsonify” bezpośrednio z modułu obsługi żądań Flask - wystarczy zwrócić oczyszczony wynik.
oferei
Masz całkowitą rację. Dykt w Pythonie (który zwraca json.loads) powinien być automatycznie jsonified przez Flask.
Garren S
Czy obiekt dyktowania nie jest wywoływalny?
SouvikMaji
@ rick112358 w jaki sposób dykt, którego nie można wywołać, odnosi się do tego pytania i odpowiedzi?
Garren S
możesz także użyć json_util.loads (), aby odzyskać dokładnie ten sam słownik (zamiast jednego z kluczem '$ oid').
rGun
22

Większość użytkowników, którzy otrzymują błąd „nie można serializować JSON”, musi po prostu określić, default=strkiedy używa json.dumps. Na przykład:

json.dumps(my_obj, default=str)

Wymusi to konwersję na str, zapobiegając błędowi. Oczywiście spójrz na wygenerowane dane wyjściowe, aby potwierdzić, że jest to, czego potrzebujesz.

Acumenus
źródło
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)

To jest przykładowy przykład konwersji BSON do obiektu JSON. Możesz tego spróbować.

vinit kantrod
źródło
16

Jako szybką wymianę możesz zmienić {'owner': objectid}na {'owner': str(objectid)}.

Ale zdefiniowanie własnego JSONEncoderjest lepszym rozwiązaniem, zależy to od twoich wymagań.

MostafaR
źródło
6

Publikowanie tutaj, jak myślę, może być przydatne dla osób korzystających Flaskz pymongo. To jest moja obecna „najlepsza praktyka” konfiguracji zezwalania kolbie na typy danych Marshall 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

Dlaczego to zamiast obsługiwać BSON lub mongod Extended JSON ?

Myślę, że obsługa mongo specjalnego JSON jest obciążeniem dla aplikacji klienckich. Większość aplikacji klienckich nie będzie przejmować się wykorzystaniem obiektów mongo w żaden złożony sposób. Jeśli obsługuję rozszerzone json, muszę teraz używać go po stronie serwera i po stronie klienta. ObjectIdi Timestampsą łatwiejsze w użyciu jako łańcuchy, a to sprawia, że ​​całe to szaleństwo mongo marshalling jest poddawane kwarantannie na serwerze.

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

Myślę, że jest to mniej uciążliwe w przypadku większości aplikacji niż.

{
  "_id": {"$oid": "5b6b6959828619572d48a9da"},
  "created_at": {"$date": 1533837843000}
}
nackjicholson
źródło
4

W ten sposób ostatnio naprawiłem błąd

    @app.route('/')
    def home():
        docs = []
        for doc in db.person.find():
            doc.pop('_id') 
            docs.append(doc)
        return jsonify(docs)
Jcc.Sanabria
źródło
w tym przypadku nie przekazujesz atrybutu „_id”, zamiast tego po prostu usunąłeś „_id” i przekazałeś inne atrybuty dokumentu
Muhriddin Ismoilov
3

Wiem, że publikuję późno, ale pomyślałem, że pomoże to przynajmniej kilku osobom!

Oba przykłady wspomniane przez tima i defuz (które są najwyżej oceniane) działają doskonale. Istnieje jednak niewielka różnica, która czasami może być znacząca.

  1. Poniższa metoda dodaje jedno dodatkowe pole, które jest nadmiarowe i może nie być idealne we wszystkich przypadkach

Pymongo zapewnia json_util - możesz użyć tego zamiast obsługi typów BSON

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

  1. Gdzie jako klasa JsonEncoder daje te same dane wyjściowe w formacie ciągu, jakiego potrzebujemy, a dodatkowo musimy użyć json.loads (wyjście). Ale to prowadzi do

Wynik: {"_id": "abc123"}

Mimo że pierwsza metoda wygląda na prostą, obie wymagają bardzo minimalnego wysiłku.

rohithnama
źródło
jest to bardzo przydatne dla pytest-mongodbwtyczki podczas tworzenia urządzeń
tsveti_iko
3

w moim przypadku potrzebowałem czegoś takiego:

class JsonEncoder():
    def encode(self, o):
        if '_id' in o:
            o['_id'] = str(o['_id'])
        return o
Mahorad
źródło
1
+1 Ha! Czy mogło być prościej 😍 Ogólnie rzecz biorąc; aby uniknąć wszelkich problemów związanych z niestandardowymi koderami i importowaniem plików bson, rzut ObjectID na ciąg :object['_id'] = str(object['_id'])
Vexy
2

Chciałbym przedstawić dodatkowe rozwiązanie poprawiające zaakceptowaną odpowiedź. Wcześniej podałem odpowiedzi w innym wątku tutaj .

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
źródło
1

Jeśli nie będziesz potrzebować identyfikatora _id rekordów, zalecam wyłączenie go podczas wysyłania zapytań do bazy danych, co umożliwi bezpośrednie drukowanie zwróconych rekordów, np

Aby cofnąć ustawienie _id podczas odpytywania, a następnie wypisywać dane w pętli, napisz coś takiego

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

ROZWIĄZANIE na: Mangusta + ptasie mleczko

Jeśli używasz mongoenginei marshamallowwtedy to rozwiązanie może być stosowane dla Ciebie.

Zasadniczo importowane Stringpole z prawoślazu oraz nadpisane I domyślnie Schema idmają być Stringzakodowane.

from marshmallow import Schema
from marshmallow.fields import String

class FrontendUserSchema(Schema):

    id = String()

    class Meta:
        fields = ("id", "email")
Łukasz Dynowski
źródło
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
źródło
0

Jeśli nie chcesz _idw odpowiedzi, możesz refaktoryzować swój kod w następujący sposób:

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

To usunie TypeError: ObjectId('') is not JSON serializablebłąd.

sarthakgupta072
źródło