I have python fastapi app with mongodb.
Below I provided schema and code to create a document.
I need to use decimal, because there’re problems with floating point in float data type. However, there are problems with using condecimal with mongo.
class FlowerTypeSchema(BaseModel):
id: UUID = None
merchant_id: Optional[UUID] = None
name: LangSchema
description: LangSchema
photo: Optional[str] = None
custom: Optional[bool] = False
rating: Optional[condecimal(ge=0)] = 0
tags: List[str]
consumables: List[ProductsSpentSchema]
created_at: datetime = None
updated_at: datetime = None
deleted_at: datetime = None
async def create_flower_type(flower_type: FlowerTypeSchema) -> FlowerTypeResponse:
flower_type.id = uuid4()
flower_type.photo = None
flower_type.created_at = datetime.now(pytz.timezone(TIMEZONE)).strftime("%Y-%m-%dT%H:%M:%S")
flower_type.updated_at = datetime.now(pytz.timezone(TIMEZONE)).strftime("%Y-%m-%dT%H:%M:%S")
await flower_types_collection.insert_one(flower_type.dict())
return FlowerTypeResponse(**flower_type.dict())
I want to use decimal for rating (and for price later), but I am getting an error:
2024-06-01 14:46:32,147 - uvicorn.error - ERROR - Exception in ASGI application
Traceback (most recent call last):
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagesuvicornprotocolshttph11_impl.py", line 408, in run_asgi
result = await app( # type: ignore[func-returns-value]
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagesuvicornmiddlewareproxy_headers.py", line 84, in __call__
return await self.app(scope, receive, send)
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagesfastapiapplications.py", line 1106, in __call__
await super().__call__(scope, receive, send)
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagesstarletteapplications.py", line 122, in __call__
await self.middleware_stack(scope, receive, send)
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagesstarlettemiddlewareerrors.py", line 184, in __call__
raise exc
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagesstarlettemiddlewareerrors.py", line 162, in __call__
await self.app(scope, receive, _send)
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagesstarlettemiddlewarecors.py", line 83, in __call__
await self.app(scope, receive, send)
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagesstarlettemiddlewareexceptions.py", line 79, in __call__
raise exc
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagesstarlettemiddlewareexceptions.py", line 68, in __call__
await self.app(scope, receive, sender)
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagesfastapimiddlewareasyncexitstack.py", line 20, in __call__
raise e
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagesfastapimiddlewareasyncexitstack.py", line 17, in __call__
await self.app(scope, receive, send)
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagesstarletterouting.py", line 718, in __call__
await route.handle(scope, receive, send)
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagesstarletterouting.py", line 276, in handle
await self.app(scope, receive, send)
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagesstarletterouting.py", line 66, in app
response = await func(request)
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagesfastapirouting.py", line 274, in app
raw_response = await run_endpoint_function(
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagesfastapirouting.py", line 191, in run_endpoint_function
return await dependant.call(**values)
File "c:UsersAkmalITOyGulog-py-merchant-content-serviceroutesflower_types.py", line 31, in create_flower_type_data
new_flower_type = await create_flower_type(flower_type=data)
File "c:UsersAkmalITOyGulog-py-merchant-content-servicedatabaseflower_types.py", line 17, in create_flower_type
await flower_types_collection.insert_one(flower_type.dict())
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libconcurrentfuturesthread.py", line 58, in run
result = self.fn(*self.args, **self.kwargs)
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagespymongocollection.py", line 669, in insert_one
self._insert_one(
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagespymongocollection.py", line 609, in _insert_one
self.__database.client._retryable_write(acknowledged, _insert_command, session)
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagespymongomongo_client.py", line 1523, in _retryable_write
return self._retry_with_session(retryable, func, s, bulk)
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagespymongomongo_client.py", line 1421, in _retry_with_session
return self._retry_internal(
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagespymongo_csot.py", line 107, in csot_wrapper
return func(self, *args, **kwargs)
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagespymongomongo_client.py", line 1462, in _retry_internal
).run()
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagespymongomongo_client.py", line 2315, in run
return self._read() if self._is_read else self._write()
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagespymongomongo_client.py", line 2422, in _write
return self._func(self._session, conn, self._retryable) # type: ignore
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagespymongocollection.py", line 597, in _insert_command
result = conn.command(
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagespymongohelpers.py", line 322, in inner
return func(*args, **kwargs)
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagespymongopool.py", line 996, in command
self._raise_connection_failure(error)
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagespymongopool.py", line 968, in command
return command(
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagespymongonetwork.py", line 151, in command
request_id, msg, size, max_doc_size = message._op_msg(
File "C:UsersAkmalAppDataLocalProgramsPythonPython310libsite-packagespymongomessage.py", line 762, in _op_msg
return _op_msg_uncompressed(flags, command, identifier, docs, opts)
bson.errors.InvalidDocument: cannot encode object: Decimal('4.5'), of type: <class 'decimal.Decimal'>
3
Answers
You are trying to use a
Decimal
field type which does not have an equivalent in JSON and BSON. So PyMongo won’t automatically convert it to something compatible. Even when using a response_model, FastAPI/Pydantic don’t have default handlers for this, unlike datetime, UUID, etc.Part 1: Conversion for MongoDB
You’ll need to convert that to a string or a
Decimal128
which can be used as a MongoDB type.Do that with
@field_serializer
. Also make use offlower_type.model_dump()
instead offlower_type.dict()
:Part 2: Conversion for FastAPI response
For the FastAPI side, since you have a
FlowerTypeResponse
as well as aFlowerTypeSchema
, you can also use that class with inheritance and overriderating
to convert to string. Like in the FastAPI docs example for In/OutUserThe problem is that pymongo can’t handle
Decimal
, but fastapi can’t handlebson.Decimal128
. So you need different serialization methods for different contexts. I had to solve the same problem for my application that handles currency values. I did it with a custom typeMoney
.Money
will take a range of input values and convert them toDecimal
. By default it will serialize to a float, so it’s usable in json output.But it offers a context switch to serialize it as
bson.Decimal128
:So in your case, when preparing a model to store into database, you would use:
I’m open for suggestions on how to improve this btw, but the good thing is that it works in a reusable way.
You can create your custom codec to work with Decimal. Check the link.
It should looks like: