David Y.
—I’m building a calendar application with FastAPI, using MongoDB for data storage. The endpoint I’ve written for event creation takes data based on an Event
model I’ve defined and should store that data in the MongoDB database and then return it to the user. However, at the moment, calls to this endpoint fail with the following errors:
ValueError: [TypeError("'ObjectId' object is not iterable"), TypeError('vars() argument must have __dict__ attribute')]
Here’s my application code:
from fastapi import FastAPI from pymongo import MongoClient from pydantic import BaseModel from datetime import datetime app = FastAPI() class Event(BaseModel): title: str description: str start_time: datetime end_time: datetime def store_in_mongo(collection: str, event: dict): client = MongoClient("mongodb://localhost:27017") db = client["calendar_db"] collection = db[collection] result = collection.insert_one(event) return result.acknowledged @app.post("/events/") async def create_event(event: Event): event_dict = event.dict() result = store_in_mongo("events", event_dict) if result: return event_dict else: return {"message": "Failed to create event."} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)
Why am I getting these errors and how can I fix them?
This error occurs because PyMongo’s insert_one
method mutates event_dict
by adding an '_id'
key with a MongoDB ObjectID
object as a value. We can see this in action by adding print statements to create_event
before and after store_in_mongo
is called:
@app.post("/events/") async def create_event(event: Event): event_dict = event.dict() print(event_dict) result = store_in_mongo("events", event_dict) print(event_dict) if result: return event_dict else: return {"message": "Failed to create event."}
If we call the endpoint now, we will see lines similar to the following in our console output:
{'title': 'Sunday Lunch', 'description': 'Lunch with family.', 'start_time': datetime.datetime(2024, 6, 30, 8, 25, 19, 294000, tzinfo=TzInfo(UTC)), 'end_time': datetime.datetime(2024, 6, 30, 14, 25, 19, 294000, tzinfo=TzInfo(UTC))} {'title': 'Sunday Lunch', 'description': 'Lunch with family.', 'start_time': datetime.datetime(2024, 6, 30, 8, 25, 19, 294000, tzinfo=TzInfo(UTC)), 'end_time': datetime.datetime(2024, 6, 30, 14, 25, 19, 294000, tzinfo=TzInfo(UTC)), '_id': ObjectId('6683bc742368f131075b745d')}
The dictionary on the second line has an additional '_id'
key. The application throws an exception when attempting to return this dictionary because FastAPI’s internal methods for converting Python dictionaries to JSON objects don’t know what to do with PyMongo’s ObjectId
s.
There are different ways to fix this error, depending on the requirements of your application. The following solutions are presented in ascending order of complexity.
If you don’t need to return the database event ID to users, remove the '_id'
key from the dictionary before returning it. For example:
@app.post("/events/") async def create_event(event: Event): event_dict = event.dict() result = store_in_mongo("events", event_dict) event_dict.pop('_id', None) # remove key from dictionary if it exists if result: return event_dict else: return {"message": "Failed to create event."}
This solution hides the internal details of our data storage from users. Depending on the use cases and audience for your application, you may wish to hide event IDs from users or create your own, separate from the MongoDB document IDs.
JSONEncoder
ClassWe can define a custom JSONEncoder
class that knows how to handle ObjectId
s. For example:
import json from bson import ObjectId # bson = binary JSON, the data format used by MongoDB class MyJSONEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, ObjectId): return str(o) # this will return the ID as a string return json.JSONEncoder.default(self, o)
To use this encoder, make the following change to create_event
:
@app.post("/events/") async def create_event(event: Event): event_dict = event.dict() result = store_in_mongo("events", event_dict) if result: return MyJSONEncoder().encode(event_dict) # use custom JSONEncoder else: return {"message": "Failed to create event."}
This solution is the most complex, but also the most flexible, and could be useful if we need to use different JSONEncoder
s for different endpoints. This approach can be used to fix JSON encoding errors for any custom object.
Tasty treats for web developers brought to you by Sentry. Get tips and tricks from Wes Bos and Scott Tolinski.
SEE EPISODESConsidered “not bad” by 4 million developers and more than 100,000 organizations worldwide, Sentry provides code-level observability to many of the world’s best-known companies like Disney, Peloton, Cloudflare, Eventbrite, Slack, Supercell, and Rockstar Games. Each month we process billions of exceptions from the most popular products on the internet.
Here’s a quick look at how Sentry handles your personal information (PII).
×We collect PII about people browsing our website, users of the Sentry service, prospective customers, and people who otherwise interact with us.
What if my PII is included in data sent to Sentry by a Sentry customer (e.g., someone using Sentry to monitor their app)? In this case you have to contact the Sentry customer (e.g., the maker of the app). We do not control the data that is sent to us through the Sentry service for the purposes of application monitoring.
Am I included?We may disclose your PII to the following type of recipients:
You may have the following rights related to your PII:
If you have any questions or concerns about your privacy at Sentry, please email us at compliance@sentry.io.
If you are a California resident, see our Supplemental notice.