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)
Defuz
источник
Отлично! У меня это сработало. У меня уже есть класс кодировщика Json, как я могу объединить его с вашим классом? Мой класс кодирования Json уже есть: '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) '
Ирфан
1
@IrfanDayan, просто добавьте if isinstance(o, ObjectId): return str(o)до returnметода default.
defuz
2
Не могли бы вы добавить from bson import ObjectId, чтобы все могли копировать-вставить еще быстрее? Спасибо!
Ливиу Чирку
@defuz Почему бы просто не использовать str? Что плохого в таком подходе?
Кевин
@defuz: Когда я пытаюсь использовать это, ObjectID удаляется, но мой ответ json разбивается на отдельные символы. Я имею в виду, что когда я печатаю каждый элемент из полученного json в цикле for, я получаю каждый символ как элемент. Есть идеи, как это решить?
Варидж Капил
120

Pymongo предоставляет json_util - вы можете использовать его вместо этого для обработки типов BSON

Тим
источник
Я согласен с @tim, это правильный способ справиться с данными BSON, поступающими от mongo. api.mongodb.org/python/current/api/bson/json_util.html
Джошуа Пауэлл,
Да, похоже, будет больше проблем, если мы будем использовать этот способ
jonprasetyo
На самом деле это лучший способ.
Рахул
14
Пример здесь был бы немного полезнее, так как это лучший способ, но связанная документация не самая удобная для новичков,
Джейк,
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, «дамп» возвращает строку, поэтому его нельзя использовать в качестве замены 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 dict (который возвращает json.loads) должен автоматически jsonified с помощью Flask.
Garren S
Разве объект dict не вызывается?
SouvikMaji
@ rick112358 как диктант, который не может быть вызван, соотносится с этим вопросом и ответом?
Garren S
вы также можете использовать json_util.loads (), чтобы получить тот же самый словарь (вместо словаря с ключом $ oid).
rGun
22

Большинству пользователей, которые получают ошибку «не сериализуемый JSON», просто нужно указать default=strпри использовании json.dumps. Например:

json.dumps(my_obj, default=str)

Это приведет к преобразованию в str, предотвращая ошибку. Конечно, затем посмотрите на сгенерированный результат, чтобы убедиться, что это то, что вам нужно.

Acumenus
источник
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 маршалировать типы данных 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 или расширенного JSON mongod ?

Я думаю, что обслуживание специального JSON для монго ложится бременем на клиентские приложения. Большинство клиентских приложений не заботятся об использовании объектов mongo каким-либо сложным образом. Если я использую расширенный json, теперь мне придется использовать его на стороне сервера и на стороне клиента. ObjectIdи с Timestampними легче работать как со строками, и это держит все это безумие монго-маршаллинга в карантине на сервере.

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

Я думаю, что для большинства приложений это менее обременительно, чем.

{
  "_id": {"$oid": "5b6b6959828619572d48a9da"},
  "created_at": {"$date": 1533837843000}
}
Nackjicholson
источник
4

Вот как я недавно исправил ошибку

    @app.route('/')
    def home():
        docs = []
        for doc in db.person.find():
            doc.pop('_id') 
            docs.append(doc)
        return jsonify(docs)
Jcc Санабрия
источник
в этом случае вы не передаете атрибут _id, вместо этого просто удалили _id и передали другие атрибуты документа
Мухриддин Исмоилов
3

Я знаю, что отправляю сообщение поздно, но подумал, что это поможет хотя бы некоторым людям!

Оба примера, упомянутые tim и defuz (которые получили наибольшее количество голосов), работают отлично. Однако есть небольшая разница, которая временами может быть значительной.

  1. Следующий метод добавляет одно дополнительное поле, которое является избыточным и может быть идеальным не во всех случаях.

Pymongo предоставляет json_util - вы можете использовать его вместо этого для обработки типов BSON

Вывод: {"_id": {"$ oid": "abc123"}}

  1. Поскольку класс JsonEncoder дает тот же вывод в строковом формате, который нам нужен, и нам нужно дополнительно использовать json.loads (output). Но это приводит к

Вывод: {"_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

Я хотел бы предложить дополнительное решение, улучшающее принятый ответ. Я ранее давал ответы в другой теме здесь .

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
источник
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 + зефир

Если вы используете 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ошибку.

sarthakgupta072
источник