Git Product home page Git Product logo

Comments (8)

roman-right avatar roman-right commented on May 21, 2024

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.

zrothberg avatar zrothberg commented on May 21, 2024

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.

roman-right avatar roman-right commented on May 21, 2024

I see. For now, I don't know, how to fix it beautifully. But this guy is in my TODO now

from beanie.

douyahu avatar douyahu commented on May 21, 2024

fastapi中,我最后是这样解决的
image
image
image
image

from beanie.

zrothberg avatar zrothberg commented on May 21, 2024

The issue is casting between types not what you posted.

from beanie.

frankie567 avatar frankie567 commented on May 21, 2024

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.

github-actions avatar github-actions commented on May 21, 2024

This issue is stale because it has been open 30 days with no activity.

from beanie.

github-actions avatar github-actions commented on May 21, 2024

This issue was closed because it has been stalled for 14 days with no activity.

from beanie.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.