Comments (8)
Hm, this is weird a little.
The next code:
import asyncio
from typing import Optional
from pydantic import BaseModel
from beanie import PydanticObjectId, Document, init_beanie
class OutTestModel(BaseModel):
id: Optional[PydanticObjectId]
name: str
class TestModel(OutTestModel, Document):
id: Optional[PydanticObjectId]
secret_message: str
async def main():
await init_beanie(
connection_string="YOUR_CONNECTION_STRING",
document_models=[TestModel])
mymodel = TestModel(name="jeff", secret_message="this is gonna break")
await mymodel.save()
print(OutTestModel.parse_obj(mymodel))
asyncio.run(main())
prints out:
id=ObjectId('61696ef6805c50ca86d286c5') name='jeff'
I'll check with fastapi a little later, why is this breaking there. But from the pydantic side, it should work.
from beanie.
So I believe it is more or less under the hood this: OutTestModel.parse_obj(mymodel.dict(alias=True)) Because by default pydantic has allow_population_by_field_name = False. So you need to use the alias to feed back into the next object.
import asyncio
from typing import Optional
from configfile import dburl
from pydantic import BaseModel
from beanie import PydanticObjectId, Document, init_beanie
class OutTestModel(BaseModel):
id: Optional[PydanticObjectId]
name: str
class TestModel(OutTestModel, Document):
id: Optional[PydanticObjectId]
secret_message: str
async def main():
await init_beanie(
connection_string=dburl,
document_models=[TestModel])
mymodel = TestModel(name="jeff", secret_message="this is gonna break")
await mymodel.save()
print(OutTestModel.parse_obj(mymodel))
print(OutTestModel.parse_obj(mymodel.dict(by_alias=True)))
asyncio.run(main())
prints
app_1 | id=ObjectId('616979d0be470f49d46fd7cc') name='jeff'
app_1 | id=None name='jeff'
from beanie.
I see. For now, I don't know, how to fix it beautifully. But this guy is in my TODO now
from beanie.
from beanie.
The issue is casting between types not what you posted.
from beanie.
Hey there 👋
I'll bump into this issue since it was reported to me on fastapi-users/fastapi-users#1000.
Reproducible example
The problem indeed occurs when you return a Beanie document and use response_model
to "downcast" it to another Pydantic model. Here is a small reproducible example:
from typing import Optional
import motor.motor_asyncio
from beanie import PydanticObjectId, Document, init_beanie
from fastapi import FastAPI
from pydantic import BaseModel
DATABASE_URL = "mongodb://localhost:27017"
client = motor.motor_asyncio.AsyncIOMotorClient(
DATABASE_URL, uuidRepresentation="standard"
)
db = client["database_name"]
class User(Document):
email: str
hashed_password: str
class UserRead(BaseModel):
id: Optional[PydanticObjectId]
email: str
app = FastAPI()
@app.get("/user", response_model=UserRead)
async def get_user():
user = await User.all().first_or_none()
return user
@app.on_event("startup")
async def on_startup():
await init_beanie(
database=db,
document_models=[
User,
],
)
user = User(email="[email protected]", hashed_password="abc")
await user.save()
If you run this and make a request to GET /user
, the id
is null
:
{
"id": null,
"email": "[email protected]"
}
Root cause in FastAPI
I was able to track down this in FastAPI and I think this behavior is caused by this part:
if isinstance(res, BaseModel):
read_with_orm_mode = getattr(res.__config__, "read_with_orm_mode", None)
if read_with_orm_mode:
# Let from_orm extract the data from this model instead of converting
# it now to a dict.
# Otherwise there's no way to extract lazy data that requires attribute
# access instead of dict iteration, e.g. lazy relationships.
return res
return res.dict(
by_alias=True,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
When FastAPI detects that a response value is a Pydantic BaseModel
(which a Beanie Document
is), it'll first transform it to a dictionary with the .dict
method and parameter by_alias
to True
. This is where we retrieve a _id
instead of id
.
Workarounds
I found two workarounds for this.
1. Leverage the read_with_orm_mode
option
This is something that was implemented specifically for SQLModel, so may be not the best solution. But as you see in the code above, it forces FastAPI to bypass the .dict
transformation.
class User(Document):
email: str
hashed_password: str
class Config:
read_with_orm_mode = True
2. Implement a root_validator
to fill id
with _id
Quite verbose though and not very intuitive:
class UserRead(BaseModel):
id: Optional[PydanticObjectId]
email: str
@root_validator(pre=True)
def fill_id(cls, values):
id = values.get("_id")
if id is not None:
values["id"] = id
return values
3. Use .from_orm
to manually build the response object
Probably the cleanest solution:
class UserRead(BaseModel):
id: Optional[PydanticObjectId]
email: str
class Config:
orm_mode = True
# ...
@app.get("/user")
async def get_user():
user = await User.all().first_or_none()
return UserRead.from_orm(user)
All in all, I'm not sure how it could be fixed in Beanie and even if it's an issue that has to be fixed by Beanie. Maybe a warning in the docs explaining this could be enough.
from beanie.
This issue is stale because it has been open 30 days with no activity.
from beanie.
This issue was closed because it has been stalled for 14 days with no activity.
from beanie.
Related Issues (20)
- ClientSession Type mismatch HOT 1
- [BUG] _previous_revision_id is gone forever HOT 1
- [BUG] pylance for set inc and delete method (function HOT 4
- [BUG] Document model with nullable indexed field cannot be initialized after v1.21 HOT 1
- Issues with initializing a View HOT 1
- [BUG] RevisionIdWasChanged is always raised when updating through FastAPI `put` route HOT 3
- Linked Documents Nested Inside Embedded Models Aren't Saved to Their Own Collections HOT 1
- [BUG] Decimal type on save HOT 4
- [BUG] beanie iterative migration with multiple migrations fails with TransientTransactionError when there are a lot of documents HOT 3
- [BUG] 'pydantic_core._pydantic_core.Url' object is not iterable HOT 2
- [BUG] save_changes doesn't save dict data correctly when dictionary key has "." in it's value HOT 3
- [BUG] Can not create dbref without id HOT 4
- [BUG] issue with caching and statemanagement HOT 4
- [BUG] Fields of type DecimalAnnotation are serialized to json as type number and deserialized as type float HOT 2
- [BUG] Error when saving Documents with Fields of type date HOT 12
- Feature Request - Check Link Validity HOT 1
- [BUG] Optional[BackLinks] return beanie.odm.fields.BackLink object HOT 5
- Pydantic "exclude" option is not working anymore HOT 5
- [BUG] Can't `Index` a `Link` HOT 3
- [BUG] Indecipherable error during first query and no registered doc HOT 2
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from beanie.