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)
- find_one with update ignores sorting HOT 7
- [BUG] Argument of type `Callable` to `json_schema_extra` raises `AttributeError` HOT 1
- [BUG] Backlinks are not populated HOT 10
- [BUG] Fetching Links in view raises AttributeError: _database_major_version HOT 2
- [BUG] Double return in sources code of class beanie.odm.fields.Link HOT 4
- [BUG] Beanie projection and Pydantic Schema do not play well together HOT 6
- [BUG] before_event not working with validate_on_save = True HOT 2
- [BUG] ModuleNotFoundError when importing models in a migration module HOT 1
- [BUG]before_event bug HOT 3
- [BUG] Missing documentation for `beanie.odm.BulkWriter` HOT 1
- [BUG] (some) type annotations for session wrong HOT 1
- [BUG] type annotation for init_beanie() should use Sequence
- [BUG] before_event of type Update HOT 4
- [BUG] mypy does not raise error when trying to access a non-existing attribute from a beanie Document. HOT 1
- [BUG] Indexed unique will not work after Field regex. HOT 1
- [BUG] Empty BackLink for `Optional[BackLink[ADoc]]` when document has no back-link HOT 1
- Concerns and Suggestions Regarding Beanie Library HOT 2
- [Doc] Document for soft delete feature
- [BUG] Order of elements in list[Link] is not preserved
- [BUG] pydantic computed properties omitted during `insert_many` operation on Document
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.