TypeError: ObjectId ('') ไม่สามารถต่ออนุกรม JSON ได้


111

การตอบกลับของฉันกลับมาจาก MongoDB หลังจากค้นหาฟังก์ชันรวมบนเอกสารโดยใช้ Python มันส่งคืนการตอบสนองที่ถูกต้องและฉันสามารถพิมพ์ได้ แต่ไม่สามารถส่งคืนได้

ข้อผิดพลาด:

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

พิมพ์:

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

แต่เมื่อฉันพยายามกลับ:

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

เป็นการโทรแบบ 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 เชื่อมต่อได้ดีและคอลเล็กชันก็อยู่ที่นั่นด้วยและฉันก็ได้ผลลัพธ์ที่คาดหวังกลับมา แต่เมื่อฉันพยายามส่งคืนมันทำให้ฉันมีข้อผิดพลาด Json ความคิดใด ๆ ในการแปลงการตอบกลับเป็น JSON ขอบคุณ

คำตอบ:


120

คุณควรกำหนดว่าคุณเป็นเจ้าของJSONEncoderและใช้มัน:

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)

นอกจากนี้ยังสามารถใช้ด้วยวิธีต่อไปนี้

json.encode(analytics, cls=JSONEncoder)

สมบูรณ์แบบ! มันได้ผลสำหรับฉัน ฉันมีคลาสตัวเข้ารหัส Json แล้วฉันจะรวมคลาสนั้นกับคลาสของคุณได้อย่างไรคลาสการเข้ารหัส Json ของฉันคือ: 'คลาส MyJsonEncoder (json.JSONEncoder): def default (self, obj): if isinstance (obj, datetime): return str (obj.strftime ("% Y-% m-% d% H:% M:% S")) ส่งคืน json.JSONEncoder.default (self, obj) '
Irfan

1
@IrfanDayan เพียงเพิ่มif isinstance(o, ObjectId): return str(o)ก่อนในวิธีการreturn default
defuz

2
คุณสามารถเพิ่มfrom bson import ObjectIdเพื่อให้ทุกคนสามารถคัดลอกวางได้เร็วขึ้น? ขอบคุณ!
Liviu Chircu

@defuz ทำไมไม่ใช้แค่str? มีอะไรผิดปกติกับแนวทางนั้น?
Kevin

@defuz: เมื่อฉันพยายามใช้สิ่งนี้ ObjectID จะถูกลบออก แต่การตอบสนอง json ของฉันแตกออกเป็นอักขระเดี่ยว ฉันหมายถึงเมื่อฉันพิมพ์แต่ละองค์ประกอบจาก json ที่เป็นผลลัพธ์ใน for loop ฉันจะได้อักขระแต่ละตัวเป็นองค์ประกอบ มีความคิดอย่างไรที่จะแก้ปัญหานี้?
Varij Kapil

120

Pymongoมีjson_util - คุณสามารถใช้อันนั้นแทนเพื่อจัดการประเภท BSON ได้


ฉันเห็นด้วยกับ @tim นี่เป็นวิธีที่ถูกต้องในการจัดการกับข้อมูล BSON ที่มาจาก Mongo api.mongodb.org/python/current/api/bson/json_util.html
Joshua Powell

ใช่ดูเหมือนจะไม่ยุ่งยากหากเราใช้วิธีนี้
jonprasetyo

นั่นเป็นวิธีที่ดีที่สุดจริงๆ
ราหุล

14
ตัวอย่างต่อไปนี้จะเป็นประโยชน์มากกว่านี้เล็กน้อยเนื่องจากเป็นวิธีที่ดีที่สุด แต่เอกสารที่เชื่อมโยงไม่ได้เป็นมิตรกับผู้ใช้มากที่สุดสำหรับ noobs
Jake

2
from bson import json_util json.loads(json_util.dumps(user_collection)) ^ สิ่งนี้ใช้งานได้หลังจากติดตั้ง python-bsonjs ด้วยpipenv 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"}}]'

ตัวอย่างที่เกิดขึ้นจริงจากjson_util

ไม่เหมือน jsonify ของ Flask "dumps" จะส่งคืนสตริงดังนั้นจึงไม่สามารถใช้แทน jsonify ของ Flask แบบ 1: 1 ได้

แต่คำถามนี้แสดงให้เห็นว่าเราสามารถทำให้เป็นอนุกรมโดยใช้ json_util.dumps () แปลงกลับเป็น dict โดยใช้ json.loads () และสุดท้ายเรียก jsonify ของ Flask

ตัวอย่าง (มาจากคำตอบของคำถามก่อนหน้า):

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

โซลูชันนี้จะแปลง ObjectId และอื่น ๆ (เช่น Binary, Code เป็นต้น) เป็นสตริงที่เทียบเท่าเช่น "$ oid"

เอาต์พุต JSON จะมีลักษณะดังนี้:

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

เพื่อชี้แจงว่าไม่จำเป็นต้องเรียก 'jsonify' โดยตรงจากตัวจัดการคำขอ Flask เพียงส่งคืนผลการฆ่าเชื้อ
oferei

คุณพูดถูกจริงๆ Python dict (ซึ่ง json.loads ส่งคืน) ควรถูก jsonified โดยอัตโนมัติโดย Flask
Garren S

ไม่ใช่วัตถุ dict ไม่สามารถเรียกใช้ได้?
SouvikMaji

@ rick112358 คำสั่งที่ไม่สามารถเรียกได้เกี่ยวข้องกับคำถามและคำตอบนี้อย่างไร
Garren S

คุณยังสามารถใช้ json_util.loads () เพื่อรับพจนานุกรมที่เหมือนกันทุกประการกลับมาได้ (แทนที่จะใช้คำที่มีคีย์ '$ oid')
rGun

22

ผู้ใช้ส่วนใหญ่ที่ได้รับข้อผิดพลาด "ไม่ JSON serializable" ก็จำเป็นต้องระบุเมื่อใช้default=str json.dumpsตัวอย่างเช่น:

json.dumps(my_obj, default=str)

ซึ่งจะบังคับให้เกิดการแปลงเพื่อstrป้องกันข้อผิดพลาด จากนั้นดูผลลัพธ์ที่สร้างขึ้นเพื่อยืนยันว่าเป็นสิ่งที่คุณต้องการ


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)

นี่คือตัวอย่างตัวอย่างสำหรับการแปลง BSON เป็นออบเจ็กต์ JSON คุณสามารถลองสิ่งนี้


16

คุณสามารถเปลี่ยน{'owner': objectid}เป็น{'owner': str(objectid)}ไฟล์.

แต่การกำหนดของคุณเองJSONEncoderเป็นทางออกที่ดีกว่าขึ้นอยู่กับความต้องการของคุณ


6

โพสต์ที่นี่ที่ผมคิดว่ามันอาจจะเป็นประโยชน์สำหรับผู้ใช้ที่มีFlask pymongoนี่คือการตั้งค่า "แนวทางปฏิบัติที่ดีที่สุด" ในปัจจุบันของฉันสำหรับการอนุญาตให้ flask กับประเภทข้อมูล 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

ทำไมทำเช่นนี้แทนของการให้บริการ BSON หรือmongod ขยาย JSON ?

ฉันคิดว่าการให้บริการ mongo พิเศษ JSON ทำให้เกิดภาระกับแอปพลิเคชันไคลเอนต์ แอปไคลเอนต์ส่วนใหญ่จะไม่สนใจการใช้วัตถุ mongo ในลักษณะที่ซับซ้อน ถ้าฉันให้บริการ json แบบขยายตอนนี้ฉันต้องใช้ฝั่งเซิร์ฟเวอร์และฝั่งไคลเอ็นต์ ObjectIdและTimestampง่ายต่อการทำงานเป็นสตริงและสิ่งนี้ช่วยให้ความบ้าคลั่งของ mongo marshalling ทั้งหมดถูกกักกันไว้ที่เซิร์ฟเวอร์

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

ฉันคิดว่านี่เป็นเรื่องยุ่งยากในการทำงานกับแอปพลิเคชันส่วนใหญ่น้อยกว่า

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

4

นี่คือวิธีที่ฉันเพิ่งแก้ไขข้อผิดพลาด

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

ในกรณีนี้คุณไม่ได้ผ่านแอตทริบิวต์ "_id" แต่เพียงลบ "_id" และส่งต่อแอตทริบิวต์อื่น ๆ ของ doc
Muhriddin Ismoilov

3

ฉันรู้ว่าฉันโพสต์ช้า แต่คิดว่ามันจะช่วยคนได้ไม่กี่คน!

ทั้งสองตัวอย่างที่ทิมและ defuz กล่าวถึง (ซึ่งได้รับการโหวตสูงสุด) ทำงานได้ดีอย่างสมบูรณ์แบบ อย่างไรก็ตามมีความแตกต่างของนาทีซึ่งอาจมีนัยสำคัญในบางครั้ง

  1. วิธีการต่อไปนี้จะเพิ่มฟิลด์พิเศษหนึ่งฟิลด์ซึ่งซ้ำซ้อนและอาจไม่เหมาะในทุกกรณี

Pymongo มี json_util - คุณสามารถใช้อันนั้นแทนเพื่อจัดการประเภท BSON ได้

เอาต์พุต: {"_id": {"$ oid": "abc123"}}

  1. โดยที่คลาส JsonEncoder ให้เอาต์พุตเดียวกันในรูปแบบสตริงตามที่เราต้องการและเราจำเป็นต้องใช้ json.loads (เอาต์พุต) เพิ่มเติม แต่ก็นำไปสู่

เอาต์พุต: {"_id": "abc123"}

แม้ว่าวิธีแรกจะดูเรียบง่าย แต่ทั้งสองวิธีนี้ต้องใช้ความพยายามน้อยมาก


สิ่งนี้มีประโยชน์มากสำหรับpytest-mongodbปลั๊กอินเมื่อสร้างการแข่งขัน
tsveti_iko

3

ในกรณีของฉันฉันต้องการสิ่งนี้:

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

1
+1 ฮา! มันจะง่ายกว่านี้ไหม😍โดยทั่วไปพูด; เพื่อหลีกเลี่ยง fuzz ทั้งหมดด้วยตัวเข้ารหัสที่กำหนดเองและการนำเข้า bson ให้ส่ง ObjectID ไปที่สตริง :object['_id'] = str(object['_id'])
Vexy

2

กระติกน้ำของ jsonify ให้ประสิทธิภาพที่เพิ่มการรักษาความปลอดภัยตามที่อธิบายในJSON การรักษาความปลอดภัย หากใช้ตัวเข้ารหัสแบบกำหนดเองกับ Flask ควรพิจารณาประเด็นที่กล่าวถึงในJSON Securityจะดีกว่า


2

ฉันต้องการให้โซลูชันเพิ่มเติมที่ช่วยปรับปรุงคำตอบที่ยอมรับ ผมได้ให้ไว้ก่อนหน้าคำตอบในหัวข้ออื่นที่นี่

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

หากคุณไม่ต้องการ _id ของระเบียนฉันจะแนะนำให้ยกเลิกการตั้งค่าเมื่อสอบถามฐานข้อมูลซึ่งจะช่วยให้คุณสามารถพิมพ์บันทึกที่ส่งคืนได้โดยตรงเช่น

หากต้องการยกเลิกการตั้งค่า _id เมื่อค้นหาและพิมพ์ข้อมูลในลูปคุณเขียนสิ่งนี้

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

0

โซลูชันสำหรับ: mongoengine + marshmallow

ถ้าคุณใช้mongoengineและmarshamallowแล้วการแก้ปัญหานี้อาจจะมีผลบังคับใช้สำหรับคุณ

โดยทั่วไปฉันนำเข้าStringช่องจากขนมหวานและฉันเขียนทับค่าเริ่มต้นSchema idที่จะStringเข้ารหัส

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

หากคุณไม่ต้องการ_idตอบกลับคุณสามารถ refactor โค้ดของคุณได้ดังนี้:

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

สิ่งนี้จะลบTypeError: ObjectId('') is not JSON serializableข้อผิดพลาด

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.