Content-Length: 343811 | pFad | http://github.com/fastapi-users/fastapi-users/issues/1440

79 `create_update_dict` generated wrong dict when using `MappedAsDataclass` · Issue #1440 · fastapi-users/fastapi-users · GitHub
Skip to content

create_update_dict generated wrong dict when using MappedAsDataclass #1440

@jd-solanki

Description

@jd-solanki

Describe the bug

Hi 👋🏻

Thanks for creating fastapi-users. It's super helpful. I prefer type hint & autosuggestions everywhere hence I always use MappedAsDataclass in my every project. However, I noticed when we use MappedAsDataclass & fastapi-users, fastapi-users throws error if we extend the user model/schema:

# Asumming I just added nullable first_name in `User` model
TypeError: __init__() got an unexpected keyword argument 'first_name'

This is because of dataclasses. I dig more and found that core problem is create_update_dict (and probably we have to update create_update_dict_superuser as well). Due to exclude_unset=True cols which are optional get removed from dict and when we pass this dict to DB Model it raises error of missing positional argument. This won't happen if we use jsonable_encoder and remove the keys manually.

To confirm the issue I also created reproducible example without fastapi-users

from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager

from fastapi import Depends, FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from sqlalchemy import String
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, mapped_column


# --- DB
class Base(DeclarativeBase, MappedAsDataclass): ...


class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True, init=False)
    first_name: Mapped[str | None] = mapped_column(String(30))


engine = create_async_engine("sqlite+aiosqlite://github.com/:memory:")
async_session_maker = async_sessionmaker(bind=engine, expire_on_commit=False)


async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with async_session_maker() as session:
        yield session


async def create_tables():
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
        print("--- creating tables...")
        await conn.run_sync(Base.metadata.create_all)


# --- Schemas
class UserCreate(BaseModel):
    first_name: str | None = None


# --- FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
    await create_tables()
    # async with async_session_maker() as db:
    #     # Perform DB operations
    yield


app = FastAPI(lifespan=lifespan)


@app.post("/users/")
async def create_user(user: UserCreate, db: AsyncSession = Depends(get_db)):
    # Using `model_dump` to convert Pydantic model to dict
    data_dict = UserCreate.model_dump(
        user,
        exclude_unset=True,
    )
    db_product = User(**data_dict)

    # Using `jsonable_encoder` to convert Pydantic model to dict
    # db_product = User(**jsonable_encoder(user))

    db.add(db_product)
    await db.commit()
    await db.refresh(db_product)

    return db_product

To Reproduce

Snippet that uses fastapi-users with MappedAsDataclass

# //github.com/ script
# requires-python = ">=3.12"
# dependencies = [
#     "aiosqlite",
#     "fastapi-users[sqlalchemy]",
#     "fastapi[standard]",
# ]
# [tool.uv]
# exclude-newer = "2024-09-18:00:00Z"
# //github.com/
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from typing import Any

from fastapi import Depends, FastAPI
from fastapi_users import BaseUserManager, FastAPIUsers, IntegerIDMixin, schemas
from fastapi_users.authentication import (
    AuthenticationBackend,
    CookieTransport,
    JWTStrategy,
)
from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase
from sqlalchemy import String
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, mapped_column


# --- DB
class Base(DeclarativeBase, MappedAsDataclass): ...


class User(Base, SQLAlchemyBaseUserTable[Mapped[int]]):
    id: Mapped[int] = mapped_column(primary_key=True, init=False, kw_only=True)
    first_name: Mapped[str | None] = mapped_column(String(30), kw_only=True)


engine = create_async_engine("sqlite+aiosqlite://github.com/:memory:")
async_session_maker = async_sessionmaker(bind=engine, expire_on_commit=False)


async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with async_session_maker() as session:
        yield session


async def create_tables():
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
        print("--- creating tables...")
        await conn.run_sync(Base.metadata.create_all)


# --- Schemas


class UserRead(schemas.BaseUser[int]):
    pass


class UserCreate(schemas.BaseUserCreate):
    first_name: str | None = None


# --- FastAPI Users
class UserManager(IntegerIDMixin, BaseUserManager[User, int]):
    reset_password_token_secret = "SECRET"
    verification_token_secret = "SECRET"


async def get_user_db(session: AsyncSession = Depends(get_db)):
    yield SQLAlchemyUserDatabase[User, Mapped[int]](session, User)


async def get_user_manager(user_db: Any = Depends(get_user_db)):
    yield UserManager(user_db)


cookie_transport = CookieTransport(cookie_max_age=3600)


def get_jwt_strategy() -> JWTStrategy[User, int]:
    return JWTStrategy(secret="SECRET", lifetime_seconds=3600)


auth_backend = AuthenticationBackend(
    name="jwt",
    transport=cookie_transport,
    get_strategy=get_jwt_strategy,
)
fastapi_users_ins = FastAPIUsers[User, int](
    get_user_manager,
    [auth_backend],
)


# --- FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
    await create_tables()
    # async with async_session_maker() as db:
    #     # Perform DB operations
    yield


app = FastAPI(lifespan=lifespan)


app.include_router(
    fastapi_users_ins.get_register_router(UserRead, UserCreate),
    prefix="/auth",
    tags=["auth"],
)


def main():
    import uvicorn

    uvicorn.run(app, host="localhost", port=8000)


if __name__ == "__main__":
    main()

Steps to reproduce the behavior:

  1. Copy above snippet
  2. Run with uv run fastapi-users-bug.py
  3. add new user with just email & pwd
    {
      "email": "user@example.com",
      "password": "string"
    }
    
  4. See error in terminal TypeError: __init__() missing 1 required keyword-only argument: 'first_name'

Expected behavior

Using MappedAsDataclass should work fine with fastapi-users

Configuration

  • Python version : 3.12.5
  • FastAPI version : 0.114.2
  • FastAPI Users version : 13.0.0

FastAPI Users configuration

# As above

Additional context

I suggest creating test cases that uses MappedAsDataclass

I also suggest adding this VSCode config: "python.analysis.typeCheckingMode": "strict", to get type errors for missing generics


Also SQLAlchemy 2.1 gonna throw error if we don't provide fastapi_users_db_sqlalchemy.SQLAlchemyBaseUserTable as a dataclass. Will raise separate issue for this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions









      ApplySandwichStrip

      pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


      --- a PPN by Garber Painting Akron. With Image Size Reduction included!

      Fetched URL: http://github.com/fastapi-users/fastapi-users/issues/1440

      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy