TypeError: ObjectId('') is not JSON serializable
Asked Answered
N

20

178

My response back from MongoDB after querying an aggregated function on document using Python, It returns valid response and i can print it but can not return it.

Error:

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

Print:

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

But When i try to return:

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

It is 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 is well connected and collection is there too and I got back valid expected result but when i try to return it gives me Json error. Any idea how to convert the response back into JSON. Thanks

Nausea answered 16/5, 2013 at 11:25 Comment(0)
V
168

You should define you own JSONEncoder and using it:

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)

It's also possible to use it in the following way.

json.encode(analytics, cls=JSONEncoder)
Vacillating answered 16/5, 2013 at 11:30 Comment(6)
Perfect! It worked for me. I already have a Json encoder class, How can i merge that with yours class?My already Json encode class is: '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)'Nausea
@IrfanDayan, just add if isinstance(o, ObjectId): return str(o) before return in method default.Vacillating
Could you add from bson import ObjectId, so everybody can copy-paste even faster? Thanks!Barrows
@Vacillating Why not just use str? What's wrong with that approach?Sidonie
@defuz: When I try to use this, ObjectID is removed, but my json response is broken into single characters. I mean when I print each element from the resulting json in a for loop I get each character as an element. Any idea how to solve this?Hamsun
return json.JSONEncoder.default(self, o) better written as return supper().default(self, o)Calve
T
234

Bson in PyMongo distribution provides json_util - you can use that one instead to handle BSON types

from bson import json_util

def parse_json(data):
    return json.loads(json_util.dumps(data))
Transept answered 23/8, 2013 at 14:41 Comment(8)
I agree with @tim, this is correct way to deal with BSON data coming from mongo. api.mongodb.org/python/current/api/bson/json_util.htmlEntomophilous
Yes, seems to be more of a hassle free if we use this wayPicket
That's the best way actually.Sexology
An example here would be a little more helpful, as this is the best way but the linked documentation isn't the most user friendly for noobsFomalhaut
from bson import json_util json.loads(json_util.dumps(user_collection)) ^ this worked after installing python-bsonjs with pipenv install python-bsonjsBeniamino
json_util comes from bson NOT from pymongo from my viewRuminant
Please fix broken link api.mongodb.org/python/current/index.htmlRuminant
Update the link: pymongo.readthedocs.io/en/stable/api/bson/json_util.htmlPiddle
V
168

You should define you own JSONEncoder and using it:

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)

It's also possible to use it in the following way.

json.encode(analytics, cls=JSONEncoder)
Vacillating answered 16/5, 2013 at 11:30 Comment(6)
Perfect! It worked for me. I already have a Json encoder class, How can i merge that with yours class?My already Json encode class is: '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)'Nausea
@IrfanDayan, just add if isinstance(o, ObjectId): return str(o) before return in method default.Vacillating
Could you add from bson import ObjectId, so everybody can copy-paste even faster? Thanks!Barrows
@Vacillating Why not just use str? What's wrong with that approach?Sidonie
@defuz: When I try to use this, ObjectID is removed, but my json response is broken into single characters. I mean when I print each element from the resulting json in a for loop I get each character as an element. Any idea how to solve this?Hamsun
return json.JSONEncoder.default(self, o) better written as return supper().default(self, o)Calve
J
59

Most users who receive the "not JSON serializable" error simply need to specify default=str when using json.dumps. For example:

json.dumps(my_obj, default=str)

This will force a conversion to str, preventing the error. Of course then look at the generated output to confirm that it is what you need.

Jeuz answered 15/4, 2019 at 20:7 Comment(3)
This should be accepted answer! It cleanly solves the ObjectId issue. As @Jeuz says make sure it doesn't impact other json entriesManeater
I agree - it's both the simplest answer and has no (obvious) side effects.Melyndamem
Easy and clean implentation without the need to use bson, if theres a desire to omit oid, and create a 'flat' dictionary (with just the _id key and remaining attributes) . Even though this returns the exscaped quotes, passing the returned escaped JSON to json.loads converts it back to a Python dictionaryHydromancy
A
53
>>> 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"}}]'

Actual example from json_util.

Unlike Flask's jsonify, "dumps" will return a string, so it cannot be used as a 1:1 replacement of Flask's jsonify.

But this question shows that we can serialize using json_util.dumps(), convert back to dict using json.loads() and finally call Flask's jsonify on it.

Example (derived from previous question's answer):

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

This solution will convert ObjectId and others (ie Binary, Code, etc) to a string equivalent such as "$oid."

JSON output would look like this:

{
  "_id": {
    "$oid": "abc123"
  }
}
Autobahn answered 19/11, 2014 at 18:38 Comment(5)
Just to clarify, no need to call 'jsonify' directly from a Flask request handler - just return the sanitized result.Drome
You're absolutely right. A Python dict (which json.loads returns) should automatically be jsonified by Flask.Autobahn
Isn't a dict object not callable?Inside
@rick112358 how does a dict not being callable relate to this Q&A?Autobahn
you can also use json_util.loads() to get the exact same dictionary back (instead of one with '$oid' key).Ultravirus
U
25
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)

This is the sample example for converting BSON into JSON object. You can try this.

Uppercase answered 17/2, 2016 at 10:27 Comment(0)
C
17

As a quick replacement, you can change {'owner': objectid} to {'owner': str(objectid)}.

But defining your own JSONEncoder is a better solution, it depends on your requirements.

Clinquant answered 16/5, 2013 at 11:30 Comment(0)
P
16

Posting here as I think it may be useful for people using Flask with pymongo. This is my current "best practice" setup for allowing flask to marshall pymongo bson data types.

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

Why do this instead of serving BSON or mongod extended JSON?

I think serving mongo special JSON puts a burden on client applications. Most client apps will not care using mongo objects in any complex way. If I serve extended json, now I have to use it server side, and the client side. ObjectId and Timestamp are easier to work with as strings and this keeps all this mongo marshalling madness quarantined to the server.

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

I think this is less onerous to work with for most applications than.

{
  "_id": {"$oid": "5b6b6959828619572d48a9da"},
  "created_at": {"$date": 1533837843000}
}
Plantar answered 9/8, 2018 at 18:24 Comment(0)
T
11

For those who need to return the data thru Jsonify with 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)
Tsang answered 8/10, 2020 at 16:52 Comment(0)
E
9

You could try:

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

Evaporimeter answered 18/3, 2021 at 12:35 Comment(0)
D
6

in my case I needed something like this:

class JsonEncoder():
    def encode(self, o):
        if '_id' in o:
            o['_id'] = str(o['_id'])
        return o
Dictatorial answered 22/3, 2019 at 9:24 Comment(1)
+1 Ha ! Could it have been more simpler 😍 Generally speaking; to avoid all the fuzz with custom encoders and bson importing, cast ObjectID to string: object['_id'] = str(object['_id'])Nicholson
G
4

This is how I've recently fixed the error

    @app.route('/')
    def home():
        docs = []
        for doc in db.person.find():
            doc.pop('_id') 
            docs.append(doc)
        return jsonify(docs)
Giovannagiovanni answered 6/2, 2017 at 1:14 Comment(1)
in this case you are not passing '_id' attribute , instead just deleted '_id' and passed other attributes of docForegone
U
4

I know I'm posting late but thought it would help at least a few folks!

Both the examples mentioned by tim and defuz(which are top voted) works perfectly fine. However, there is a minute difference which could be significant at times.

  1. The following method adds one extra field which is redundant and may not be ideal in all the cases

Pymongo provides json_util - you can use that one instead to handle BSON types

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

  1. Where as the JsonEncoder class gives the same output in the string format as we need and we need to use json.loads(output) in addition. But it leads to

Output: { "_id": "abc123" }

Even though, the first method looks simple, both the method need very minimal effort.

Unconcern answered 12/9, 2017 at 11:44 Comment(1)
this is very useful for the pytest-mongodb plugin when creating fixturesMonday
S
4

I would like to provide an additional solution that improves the accepted answer. I have previously provided the answers in another thread here.

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()
Smudge answered 5/7, 2019 at 19:41 Comment(0)
K
4

If you want to send it as a JSON response you need to format in two steps

  1. Using json_util.dumps() from bson to covert ObjectId in BSON response to JSON compatible format i.e. "_id": {"$oid": "123456789"}

The above JSON Response obtained from json_util.dumps() will have backslashes and quotes

  1. To remove backslashes and quotes use json.loads() from 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"}]

Koons answered 10/2, 2022 at 10:50 Comment(0)
A
3

If you will not be needing the _id of the records I will recommend unsetting it when querying the DB which will enable you to print the returned records directly e.g

To unset the _id when querying and then print data in a loop you write something like this

records = mycollection.find(query, {'_id': 0}) #second argument {'_id':0} unsets the id from the query
for record in records:
    print(record)
Antoneantonella answered 25/10, 2018 at 5:28 Comment(0)
A
2

Flask's jsonify provides security enhancement as described in JSON Security. If custom encoder is used with Flask, its better to consider the points discussed in the JSON Security

Applique answered 25/3, 2015 at 10:59 Comment(0)
C
2

If you don't want _id in response, you can refactor your code something like this:

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

This will remove the TypeError: ObjectId('') is not JSON serializable error.

Chlo answered 18/7, 2020 at 19:1 Comment(0)
B
1
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))
Baneful answered 17/7, 2020 at 18:4 Comment(0)
A
1
    @app.route('/users', methods = ['GET'])
       def get_users():
       online_users = mongo.db.users.find({})
       list_users = list(online_users)
       json_users = json.dumps(list_users, default=json_util.default)
       return Response(json_users, mimetype='application/json')

This is how I made it working

Anagram answered 1/3 at 11:50 Comment(0)
P
0

SOLUTION for: mongoengine + marshmallow

If you use mongoengine and marshamallow then this solution might be applicable for you.

Basically, I imported String field from marshmallow, and I overwritten default Schema id to be String encoded.

from marshmallow import Schema
from marshmallow.fields import String

class FrontendUserSchema(Schema):

    id = String()

    class Meta:
        fields = ("id", "email")
Pedal answered 11/3, 2019 at 11:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.