-
-
Notifications
You must be signed in to change notification settings - Fork 458
Description
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:
- Copy above snippet
- Run with
uv run fastapi-users-bug.py
- add new user with just email & pwd
{ "email": "user@example.com", "password": "string" }
- 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.