TypeError: ObjectId ('') tidak dapat bersambung JSON

109

Tanggapan saya kembali dari MongoDB setelah menanyakan fungsi agregat pada dokumen menggunakan Python, Ia mengembalikan tanggapan yang valid dan saya dapat mencetaknya tetapi tidak dapat mengembalikannya.

Kesalahan:

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

Mencetak:

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

Tetapi ketika saya mencoba untuk kembali:

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

Ini adalah panggilan 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 terhubung dengan baik dan koleksi ada di sana juga dan saya mendapatkan kembali hasil yang diharapkan yang valid tetapi ketika saya mencoba mengembalikannya memberi saya kesalahan Json. Ada ide bagaimana mengubah respon kembali menjadi JSON. Terima kasih

Irfan
sumber

Jawaban:

118

Anda harus mendefinisikan Anda memiliki JSONEncoderdan menggunakannya:

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)

Itu juga memungkinkan untuk menggunakannya dengan cara berikut.

json.encode(analytics, cls=JSONEncoder)
defuz
sumber
Sempurna! Itu berhasil untuk saya. Saya sudah memiliki kelas encoder Json, Bagaimana cara menggabungkannya dengan kelas Anda? Kelas encode Json saya yang sudah ada adalah: '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, tambahkan saja if isinstance(o, ObjectId): return str(o)sebelumnya returndi metode default.
defuz
2
Bisakah Anda menambahkan from bson import ObjectId, sehingga semua orang dapat menyalin-tempel lebih cepat? Terima kasih!
Liviu Chircu
@defuz Mengapa tidak menggunakan saja str? Apa yang salah dengan pendekatan itu?
Kevin
@defuz: Ketika saya mencoba menggunakan ini, ObjectID dihapus, tetapi tanggapan json saya dipecah menjadi karakter tunggal. Maksud saya ketika saya mencetak setiap elemen dari json yang dihasilkan di loop for saya mendapatkan setiap karakter sebagai elemen. Ada ide untuk mengatasi ini?
Varij Kapil
119

Pymongo menyediakan json_util - Anda dapat menggunakannya sebagai gantinya untuk menangani jenis BSON

tim
sumber
Saya setuju dengan @tim, ini adalah cara yang benar untuk menangani data BSON yang berasal dari mongo. api.mongodb.org/python/current/api/bson/json_util.html
Joshua Powell
Ya, sepertinya akan lebih mudah jika kita menggunakan cara ini
jonprasetyo
Itu sebenarnya cara terbaik.
Rahul
14
Contoh di sini akan sedikit lebih membantu, karena ini adalah cara terbaik tetapi dokumentasi yang ditautkan bukan yang paling ramah pengguna untuk pemula
Jake
2
from bson import json_util json.loads(json_util.dumps(user_collection)) ^ ini berfungsi setelah menginstal python-bsonjs denganpipenv 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"}}]'

Contoh sebenarnya dari json_util .

Tidak seperti jsonify Flask, "dumps" akan mengembalikan string, jadi tidak bisa digunakan sebagai pengganti 1: 1 dari jsonify Flask.

Tetapi pertanyaan ini menunjukkan bahwa kita dapat membuat serial menggunakan json_util.dumps (), mengkonversi kembali ke dict menggunakan json.loads () dan terakhir memanggil jsonify Flask di atasnya.

Contoh (berasal dari jawaban pertanyaan sebelumnya):

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

Solusi ini akan mengonversi ObjectId dan lainnya (yaitu Biner, Kode, dll) menjadi string yang setara seperti "$ oid."

Keluaran JSON akan terlihat seperti ini:

{
  "_id": {
    "$oid": "abc123"
  }
}
Garren S
sumber
Hanya untuk memperjelas, tidak perlu memanggil 'jsonify' langsung dari penangan permintaan Flask - cukup kembalikan hasil yang sudah dibersihkan.
oferei
Anda benar sekali. Diktik Python (yang dikembalikan oleh json.loads) harus secara otomatis di-jsonifikasi oleh Flask.
Garren S
Bukankah objek dict tidak bisa dipanggil?
SouvikMaji
@ rick112358 bagaimana sebuah dict yang tidak dapat dipanggil berhubungan dengan T&J ini?
Garren S
Anda juga dapat menggunakan json_util.loads () untuk mendapatkan kembali kamus yang sama persis (bukan kamus dengan kunci '$ 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)

Ini adalah contoh contoh untuk mengubah BSON menjadi objek JSON. Anda bisa mencobanya.

vinit kantrod
sumber
21

Sebagian besar pengguna yang menerima kesalahan "tidak dapat disambung JSON" hanya perlu menentukan default=strsaat menggunakan json.dumps. Sebagai contoh:

json.dumps(my_obj, default=str)

Ini akan memaksa konversi ke str, mencegah kesalahan. Tentu saja kemudian lihat keluaran yang dihasilkan untuk memastikan bahwa itu yang Anda butuhkan.

Acumenus
sumber
16

Sebagai pengganti cepat, Anda dapat beralih {'owner': objectid}ke {'owner': str(objectid)}.

Tetapi menentukan milik Anda sendiri JSONEncoderadalah solusi yang lebih baik, itu tergantung pada kebutuhan Anda.

MostafaR
sumber
6

Posting di sini karena menurut saya mungkin berguna untuk orang yang menggunakan Flaskwith pymongo. Ini adalah pengaturan "praktik terbaik" saya saat ini untuk mengizinkan flask mengatur tipe data 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

Mengapa ini alih-alih melayani BSON atau mongod extended JSON ?

Menurut saya, melayani mongo khusus JSON membebani aplikasi klien. Sebagian besar aplikasi klien tidak akan peduli menggunakan objek mongo dengan cara yang rumit. Jika saya melayani json diperpanjang, sekarang saya harus menggunakannya di sisi server, dan sisi klien. ObjectIddan Timestamplebih mudah untuk digunakan sebagai string dan ini membuat semua kegilaan mongo marshalling ini dikarantina ke server.

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

Saya pikir ini tidak terlalu memberatkan untuk bekerja dengan sebagian besar aplikasi daripada.

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

Beginilah cara saya baru-baru ini memperbaiki kesalahan

    @app.route('/')
    def home():
        docs = []
        for doc in db.person.find():
            doc.pop('_id') 
            docs.append(doc)
        return jsonify(docs)
Jcc. Sanabria
sumber
dalam hal ini Anda tidak mengirimkan atribut '_id', melainkan hanya menghapus '_id' dan meneruskan atribut lain dari doc
Muhriddin Ismoilov
3

Saya tahu saya terlambat memposting tetapi saya pikir itu akan membantu setidaknya beberapa orang!

Kedua contoh yang disebutkan oleh tim dan defuz (yang dipilih terbanyak) berfungsi dengan baik. Namun, ada perbedaan menit yang terkadang bisa signifikan.

  1. Metode berikut menambahkan satu bidang tambahan yang berlebihan dan mungkin tidak ideal di semua kasus

Pymongo menyediakan json_util - Anda dapat menggunakannya sebagai gantinya untuk menangani jenis BSON

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

  1. Sedangkan kelas JsonEncoder memberikan keluaran yang sama dalam format string seperti yang kita butuhkan dan kita perlu menggunakan json.loads (keluaran) sebagai tambahan. Tapi itu mengarah ke

Keluaran: {"_id": "abc123"}

Meski cara pertama terlihat sederhana, kedua cara tersebut membutuhkan tenaga yang sangat minim.

rohithnama
sumber
ini sangat berguna untuk pytest-mongodbplugin saat membuat perlengkapan
tsveti_iko
3

dalam kasus saya, saya membutuhkan sesuatu seperti ini:

class JsonEncoder():
    def encode(self, o):
        if '_id' in o:
            o['_id'] = str(o['_id'])
        return o
Mahorad
sumber
1
+1 Ha! Mungkinkah lebih sederhana Secara umum; untuk menghindari semua ketidakjelasan dengan pembuat enkode khusus dan impor bson, gunakan ObjectID ke string :object['_id'] = str(object['_id'])
Vexy
2

Jsonify Flask menyediakan peningkatan keamanan seperti yang dijelaskan dalam Keamanan JSON . Jika pembuat enkode khusus digunakan dengan Flask, sebaiknya pertimbangkan poin-poin yang dibahas dalam Keamanan JSON

Anish
sumber
2

Saya ingin memberikan solusi tambahan yang meningkatkan jawaban yang diterima. Saya sebelumnya telah memberikan jawaban di utas lain di sini .

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

Jika Anda tidak memerlukan _id dari catatan, saya akan merekomendasikan untuk tidak menyetelnya saat menanyakan DB yang akan memungkinkan Anda untuk mencetak catatan yang dikembalikan secara langsung, mis.

Untuk menghapus _id saat membuat kueri dan kemudian mencetak data dalam satu lingkaran, Anda menulis sesuatu seperti ini

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

SOLUSI untuk: mongoengine + marshmallow

Jika Anda menggunakan mongoenginedan marshamallowkemudian solusi ini mungkin berlaku untuk Anda.

Pada dasarnya, saya mengimpor Stringbidang dari marshmallow, dan saya menimpa default Schema iduntuk Stringdikodekan.

from marshmallow import Schema
from marshmallow.fields import String

class FrontendUserSchema(Schema):

    id = String()

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

Jika Anda tidak ingin _idmenanggapi, Anda dapat memfaktorkan ulang kode Anda seperti ini:

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

Ini akan menghapus TypeError: ObjectId('') is not JSON serializablekesalahan.

sarthakgupta072
sumber