Ошибка типа: ObjectId('') не сериализуем в формате JSON

Мой ответ от 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

Это ОТЛИЧНЫЙ звонок:

@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

БД хорошо подключен, и коллекция тоже есть, и я получил верный ожидаемый результат, но когда я пытаюсь вернуться, он дает мне ошибку Json. Любая идея, как преобразовать ответ обратно в JOSON. Спасибо

19 ответов

Решение

Вы должны определить свой собственный 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)

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

Большинству пользователей, которые получают ошибку "not JSON serializable", просто необходимо указать default=str когда используешь json.dumps, Например:

json.dumps(my_obj, default=str)

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

>>> 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 и другие (например, двоичные, код и т. Д.) В строковый эквивалент, такой как "$oid".

Вывод JSON будет выглядеть так:

{
  "_id": {
    "$oid": "abc123"
  }
}
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. Вы можете попробовать это.

В качестве быстрой замены вы можете изменить {'owner': objectid} в {'owner': str(objectid)},

Но определяя свой JSONEncoder это лучшее решение, это зависит от ваших требований.

Размещение здесь, так как я думаю, что это может быть полезно для людей, использующих 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

Почему это вместо того, чтобы обслуживать расширенный JSON или BSON?

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

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

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

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

Для тех, кому нужно вернуть данные через Jsonify с Flask:

cursor = db.collection.find()
data = []
for doc in cursor:
    doc['_id'] = str(doc['_id']) # This does the trick!
    data.append(doc)
return jsonify(data)

Вы можете попробовать:

objectid = str(ObjectId("51948e86c25f4b1d1c0d303c"))

В моем случае мне нужно что-то вроде этого:

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

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

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

Я знаю, что пишу поздно, но думал, что это поможет, по крайней мере, нескольким людям!

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

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

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

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

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

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

Хотя первый метод выглядит простым, оба метода требуют минимальных усилий.

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

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()

Для получающих\и ненужных ""

Если вы хотите отправить его как ответ JSON, вам нужно отформатировать его в два этапа.

  1. Использование от bson до скрытого ObjectIdв ответе BSON на формат, совместимый с JSON, т.е. "_id": {"$oid": "123456789"}

Приведенный выше ответ JSON, полученный от json_util.dumps()будет иметь обратную косую черту и кавычки

  1. Чтобы удалить обратную косую черту и кавычки, используйте json.loads()из json
      from bson import json_util
import json

bson_data = [{'_id': ObjectId('123456789'), 'field': 'somedata'},{'_id': ObjectId('123456781'), 'field': 'someMoredata'}]

json_data_with_backslashes = json_util.dumps(bson_data)

# output will look like this
# "[{\"_id\": {\"$oid\": \"123456789\"}, \"field\": \"somedata\"},{\"_id\": {\"$oid\": \"123456781\"}, \"field\": \"someMoredata\"}]"

json_data = json.loads(json_data_with_backslashes)

# output will look like this
# [{"_id": {"$oid": "123456789"},"field": "somedata"},{"_id": {"$oid": "123456781"},"field": "someMoredata"}]

Если вам не понадобится _id записей, я рекомендую отключить его при запросе к БД, что позволит вам напечатать возвращенные записи напрямую, например

Чтобы сбросить _id при запросе, а затем распечатать данные в цикле, вы пишете что-то вроде этого

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

Jsonify Flask обеспечивает повышение безопасности, как описано в JSON Security. Если с Flask используется пользовательский кодировщик, лучше рассмотреть вопросы, обсуждаемые в JSON Security

Если ты не хочешь _id в ответ вы можете провести рефакторинг своего кода примерно так:

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

Это удалит TypeError: ObjectId('') is not JSON serializable ошибка.

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))

РЕШЕНИЕ для: 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")
Другие вопросы по тегам