Flask, проблема с JSONEncoder для словаря
Всем привет.
Помогите, пожалуйста, разобраться почему возникает ошибка во Flask при попытке вернуть словарь.
В роут приходит запрос. Исходя из запроса забираю данные в МонгоДБ и возвращаю словарь. Но почему-то return самого роута выкидывает ошибку:
flask_app_bsg_antispam | File "/usr/local/lib/python3.7/site-packages/flask/json/__init__.py", line 100, in default
flask_app_bsg_antispam | return _json.JSONEncoder.default(self, o)
flask_app_bsg_antispam | File "/usr/local/lib/python3.7/json/encoder.py", line 179, in default
flask_app_bsg_antispam | raise TypeError(f'Object of type {o.__class__.__name__} '
nginx_bsg_antispam | 172.19.0.1 - - [25/Jul/2021:05:25:31 +0000] "GET /api/v1/get_data?page_num=1 HTTP/1.1" 500 141 "-" "PostmanRuntime/7.28.0" "-"
flask_app_bsg_antispam | TypeError: Object of type ObjectId is not JSON serializable
Запрос из роута в Монго и попытка вернуть ответ такой:
@blueprint.route('/api/v1/get_data', methods=['GET'])
def v1_get_data():
page_size = request.args.get('page_size', default=25, type=int)
page_num = request.args.get('page_num', default=1, type=int)
filter = m.prepare_filter(spam=request.args.get('spam'), from_cache=request.args.get('from_cache'))
data = m.get_paginated_data(page_size, page_num, filter)
return data
Сам словарь из Монго выглядит так:
flask_app_bsg_antispam | data: {'data': [{'_id': ObjectId('60e422a7c37c8d5c7f92add9'), 'processing_duration': 378.8, 'from_cache': False, 'predict_result': 'Spam', 'probability': '0.5316912313644865', 'message_hash': '8ee640883e8bce3ebed1dba8a0c6c7e5', 'date': '2021-07-06 09:30:12.193452'}, {'_id': ObjectId('60e422b1c37c8d5c7f92addb'), 'processing_duration': 49.75, 'from_cache': False, 'predict_result': 'Spam', 'probability': '0.7002104', 'message_hash': 'b0384a99d40dfe6375d615689e02dfb2', 'date': '2021-07-06 09:30:24.465770'}, {'_id': ObjectId('60e422b1c37c8d5c7f92addc'), 'processing_duration': 35.61, 'from_cache': False, 'predict_result': 'Spam', 'probability': '0.6056733', 'message_hash': '6d7b2640368d0b98dc81a3cde41b7ed9', 'date': '2021-07-06 09:30:17.535144'}, {'_id': ObjectId('60e422c5c37c8d5c7f92addf'), 'processing_duration': 17.57, 'from_cache': False, 'predict_result': 'Spam', 'probability': '0.8142542043496809', 'message_hash': '910be0727279462f6cca4d5b43093387', 'date': '2021-07-06 09:30:42.515050'}, {'_id': ObjectId('60e423b5c37c8d5c7f92adf8'), 'processing_duration': 1.08, 'from_cache': True, 'predict_result': 'Spam', 'probability': '0.8142542043496809', 'message_hash': '910be0727279462f6cca4d5b43093387', 'date': '2021-07-06 09:34:38.985356'}, {'_id': ObjectId('60e42f6ec37c8d5c7f92af25'), 'processing_duration': 14.71, 'from_cache': False, 'predict_result': 'Spam', 'probability': '0.7841739770961472', 'message_hash': 'ad2f8a932272db08debaff29cb8feb69', 'date': '2021-07-06 10:24:40.329178'}, {'_id': ObjectId('60e42f78c37c8d5c7f92af27'), 'processing_duration': 5.93, 'from_cache': False, 'predict_result': 'Spam', 'probability': '0.7841739770961472', 'message_hash': 'd8d6cbc88b02d4dc4adb465d76843c00', 'date': '2021-07-06 10:24:48.530278'}, {'_id': ObjectId('60e42f82c37c8d5c7f92af29'), 'processing_duration': 9.18, 'from_cache': False, 'predict_result': 'Ham', 'probability': '0.5042150298482148', 'message_hash': 'd35586e8dddff88c41c9ed8e36179a49', 'date': '2021-07-06 10:25:03.079936'}, {'_id': ObjectId('60e42fb4c37c8d5c7f92af2f'), 'processing_duration': 8.23, 'from_cache': False, 'predict_result': 'Ham', 'probability': '0.5042150298482148', 'message_hash': 'c61e1179249679a7789144cf8d3b5afe', 'date': '2021-07-06 10:25:46.875858'}], 'pages': 1}
Я пока что реализовал так:
# Convert object type for _id to string
for key, value in data.items():
if key == 'data':
for d in value:
d['_id'] = str(d.get('_id'))
Не знаю насколько это оптимальный код.
Ответы (2 шт):
В словаре находится в том числе и объект типа ObjectId. По умолчанию, Питон не знает, как сериализовать этот тип.
Одно возможное решение, это использовать собственный JSON encoder, где вы можете определить, что именно нужно делать с ObjectId.
import datetime
import simplejson
class JsonEncoder(simplejson.JSONEncoder):
def default(self, obj):
if isinstance(obj, ObjectId):
return str(obj)
return json.JSONEncoder.default(self, obj)
Чтобы encoder использовался программой, нужно сообщить о нем Flask-у. Это делается вот так:
app = flask.Flask(__name__)
...
app.json_encoder = JsonEncoder
Альтернативное решение, это трансформация объекта их базы данных в объект типа, который определен в business layer вашей программы (и который не использует типы определенной базы данных). Обычно делается именно так. Если вам незнакомо разделение на data access layer, business layer и presentation layer, вам возможно будет интересна статья Многоуровневая архитектура из Википедии.
Если вам не нужно поле "_id", отсеките его при поиске в БД указанием возвращаемых полей. Если используете pymongo: .find({параметры поиска}, {'_id': 0})