TypeError: ObjectId ('') - не серіалізаційний JSON


109

Моя відповідь від 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 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 добре пов’язаний і колекція теж є, і я повернув дійсний очікуваний результат, але коли я намагаюся повернути, це дає мені помилку Json. Будь-яка ідея, як перетворити відповідь назад в JSON. Дякую

Відповіді:


118

Ви повинні визначити, що ви є власником 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 є: 'class MyJsonEncoder (json.JSONEncoder): def default (self, obj): if isin substance (obj, datetime): return str (obj.strearch ("% Y-% m-% d% H:% M:% S")) return json.JSONEncoder.default (self, obj) '
Irfan

1
@IrfanDayan, просто додайте if isinstance(o, ObjectId): return str(o)раніше returnв метод default.
дефуз

2
Чи можете ви додати from bson import ObjectId, щоб кожен міг скопіювати та вставити ще швидше? Дякую!
Liviu Chircu

@defuz Чому б не просто використовувати str? Що з цим підходом?
Кевін

@defuz: Коли я намагаюся використовувати це, ObjectID видаляється, але моя відповідь json розбита на окремі символи. Я маю на увазі, коли я друкую кожен елемент із отриманого json в циклі for for, я отримую кожен символ як елемент. Будь-яка ідея, як це вирішити?
Варай Капіль

119

Pymongo надає json_util - ви можете використовувати його замість цього для обробки типів BSON


Я погоджуюся з @tim, це правильний спосіб поводження з даними BSON, що надходять від mongo. api.mongodb.org/python/current/api/bson/json_util.html
Джошуа Пауелл

Так, здається, більше клопоту, якщо ми використовуємо цей спосіб
jonprasetyo

Це насправді найкращий спосіб.
Рахул

14
Приклад тут був би трохи кориснішим, оскільки це найкращий спосіб, але зв'язана документація не є найзручнішою для нообів
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" поверне рядок, тому його не можна використовувати як заміну 1: 1 на jsonify Flask.

Але це питання показує, що ми можемо серіалізувати за допомогою 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 та інші (тобто Бінарний, Код тощо) в еквівалент рядка, наприклад "$ oid".

Вихід JSON виглядатиме так:

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

Для уточнення, не потрібно дзвонити 'jsonify' безпосередньо від обробника запитів на Flask - просто поверніть санізований результат.
oferei

Ви абсолютно праві. Дік Python (який повертає json.loads) повинен автоматично бути об'єднаний Flask.
Гаррен S

Чи не об'єкт dict не може називатися?
SouvikMaji

@ rick112358, як диктант не може називатися відноситься до цього запитання?
Гаррен 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 . Це моє поточне налаштування "найкращої практики", що дозволяє колбі переглядати типи даних 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 ?

Я думаю, що обслуговування монго спеціального JSON накладає тягар на клієнтські програми. Більшість клієнтських додатків не переймається використанням об'єктів mongo будь-яким складним способом. Якщо я обслуговую розширений json, тепер мені доведеться використовувати його як на сервері, так і на стороні клієнта. ObjectIdі Timestampїх легше працювати як рядки, і це зберігає все це монголо, що скасовує безумство на карантин для сервера.

{
  "_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
Мухріддін Ісмоїлов

3

Я знаю, що я відправлю пізно, але подумав, що це допоможе принаймні кілька людей!

Обидва приклади, згадані тим і дефуз (за які голосують вгорі), працюють чудово. Однак є різниця в хвилині, яка в рази може бути значною.

  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 Га! Чи могло бути простіше 😍 Взагалі кажучи; щоб уникнути всіх нечітких даних із користувацькими кодерами та імпортом bson , передавайте ObjectID на рядок :object['_id'] = str(object['_id'])
Vexy

2

Jsonify Flask забезпечує підвищення безпеки, як описано в JSON Security . Якщо з 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і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у відповідь ви не хочете , ви можете перефактурувати свій код приблизно так:

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

Це призведе до усунення TypeError: ObjectId('') is not JSON serializableпомилки.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.