Splitting sqlmodel example code in multiple files results in PydanticUserError #1283
-
First Check
Commit to Help
Example Code(the only effective change I did seems to be changing the type signature of the fields in Splitting up the example code from https://sqlmodel.tiangolo.com/tutorial/fastapi/relationships/ to multiple files, i.e. in the following way: # heroes/models.py from typing import TYPE_CHECKING from sqlmodel import Field, Relationship, SQLModel if TYPE_CHECKING: from teams.models import Team, TeamPublic class HeroBase(SQLModel): name: str = Field(index=True) secret_name: str age: int | None = Field(default=None, index=True) team_id: int | None = Field(default=None, foreign_key="team.id") class Hero(HeroBase, table=True): id: int | None = Field(default=None, primary_key=True) team: "Team | None" = Relationship(back_populates="heroes") class HeroPublic(HeroBase): id: int class HeroCreate(HeroBase): pass class HeroUpdate(SQLModel): name: str | None = None secret_name: str | None = None age: int | None = None team_id: int | None = None class HeroPublicWithTeam(HeroPublic): team: "TeamPublic | None" = None# heroes/views.py from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlmodel import select from databases import get_session from heroes.models import ( Hero, HeroCreate, HeroPublic, HeroPublicWithTeam, HeroUpdate, ) router = APIRouter() @router.post("/heroes/", response_model=HeroPublic) async def create_hero( *, session: AsyncSession = Depends(get_session), hero: HeroCreate ): db_hero = Hero.model_validate(hero) session.add(db_hero) await session.commit() await session.refresh(db_hero) return db_hero @router.get("/heroes/", response_model=list[HeroPublic]) async def read_heroes( *, session: AsyncSession = Depends(get_session), offset: int = 0, limit: int = Query(default=100, le=100), ): heroes = await session.execute(select(Hero).offset(offset).limit(limit)) return heroes.all() @router.get("/heroes/{hero_id}", response_model=HeroPublicWithTeam) async def read_hero(*, session: AsyncSession = Depends(get_session), hero_id: int): hero = await session.get(Hero, hero_id) if not hero: raise HTTPException(status_code=404, detail="Hero not found") return hero @router.patch("/heroes/{hero_id}", response_model=HeroPublic) async def update_hero( *, session: AsyncSession = Depends(get_session), hero_id: int, hero: HeroUpdate ): db_hero = await session.get(Hero, hero_id) if not db_hero: raise HTTPException(status_code=404, detail="Hero not found") hero_data = hero.model_dump(exclude_unset=True) for key, value in hero_data.items(): setattr(db_hero, key, value) session.add(db_hero) await session.commit() await session.refresh(db_hero) return db_hero @router.delete("/heroes/{hero_id}") async def delete_hero(*, session: AsyncSession = Depends(get_session), hero_id: int): hero = await session.get(Hero, hero_id) if not hero: raise HTTPException(status_code=404, detail="Hero not found") await session.delete(hero) await session.commit() return {"ok": True}# teams/models.py from typing import List, TYPE_CHECKING from sqlmodel import Field, Relationship, SQLModel if TYPE_CHECKING: from heroes.models import Hero, HeroPublic class TeamBase(SQLModel): name: str = Field(index=True) headquarters: str class Team(TeamBase, table=True): id: int | None = Field(default=None, primary_key=True) heroes: List["Hero"] = Relationship(back_populates="team") class TeamCreate(TeamBase): pass class TeamPublic(TeamBase): id: int class TeamUpdate(SQLModel): id: int | None = None name: str | None = None headquarters: str | None = None class TeamPublicWithHeroes(TeamPublic): heroes: List["HeroPublic"] = []# teams/views.py from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlmodel import select from databases import get_session from teams.models import ( Team, TeamCreate, TeamPublic, TeamPublicWithHeroes, TeamUpdate, ) router = APIRouter() @router.post("/teams/", response_model=TeamPublic) async def create_team( *, session: AsyncSession = Depends(get_session), team: TeamCreate ): db_team = Team.model_validate(team) session.add(db_team) await session.commit() await session.refresh(db_team) return db_team @router.get("/teams/", response_model=list[TeamPublic]) async def read_teams( *, session: AsyncSession = Depends(get_session), offset: int = 0, limit: int = Query(default=100, le=100), ): teams = await session.execute(select(Team).offset(offset).limit(limit)) return teams.all() @router.get("/teams/{team_id}", response_model=TeamPublicWithHeroes) async def read_team(*, team_id: int, session: AsyncSession = Depends(get_session)): team = await session.get(Team, team_id) if not team: raise HTTPException(status_code=404, detail="Team not found") return team @router.patch("/teams/{team_id}", response_model=TeamPublic) async def update_team( *, session: AsyncSession = Depends(get_session), team_id: int, team: TeamUpdate, ): db_team = await session.get(Team, team_id) if not db_team: raise HTTPException(status_code=404, detail="Team not found") team_data = team.model_dump(exclude_unset=True) for key, value in team_data.items(): setattr(db_team, key, value) session.add(db_team) await session.commit() await session.refresh(db_team) return db_team @router.delete("/teams/{team_id}") async def delete_team(*, session: AsyncSession = Depends(get_session), team_id: int): team = await session.get(Team, team_id) if not team: raise HTTPException(status_code=404, detail="Team not found") await session.delete(team) await session.commit() return {"ok": True}# databases.py from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker from sqlmodel.ext.asyncio.session import AsyncSession from src.config import settings engine = create_async_engine(settings.DATABASE_URL, echo=settings.DB_ECHO) async def get_session(): async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) async with async_session() as session: yield session# main.py from fastapi import FastAPI from heroes.views import router as heroes_router from teams.views import router as teams_router from databases import run_migrations app = FastAPI( title="Debug API", description="Minimal API for debugging heroes and teams", version="0.1.0", ) # Include only heroes and teams routers app.include_router(heroes_router, tags=["Heroes"]) app.include_router(teams_router, tags=["Teams"]) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="127.0.0.1", port=8000)DescriptionWill result in the following error when attempting to view the swagger spec: INFO: 127.0.0.1:43332 - "GET /api/docs HTTP/1.1" 200 OK INFO: 127.0.0.1:43332 - "GET /api/openapi.json HTTP/1.1" 500 Internal Server Error ERROR: Exception in ASGI application Traceback (most recent call last): File ".../.venv/lib/python3.12/site-packages/uvicorn/protocols/http/httptools_impl.py", line 409, in run_asgi result = await app( # type: ignore[func-returns-value] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ".../.venv/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py", line 60, in __call__ return await self.app(scope, receive, send) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ".../.venv/lib/python3.12/site-packages/fastapi/applications.py", line 1054, in __call__ await super().__call__(scope, receive, send) File ".../.venv/lib/python3.12/site-packages/starlette/applications.py", line 113, in __call__ await self.middleware_stack(scope, receive, send) File ".../.venv/lib/python3.12/site-packages/starlette/middleware/errors.py", line 187, in __call__ raise exc File ".../.venv/lib/python3.12/site-packages/starlette/middleware/errors.py", line 165, in __call__ await self.app(scope, receive, _send) File ".../.venv/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 62, in __call__ await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) File ".../.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app raise exc File ".../.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app await app(scope, receive, sender) File ".../.venv/lib/python3.12/site-packages/starlette/routing.py", line 715, in __call__ await self.middleware_stack(scope, receive, send) File ".../.venv/lib/python3.12/site-packages/starlette/routing.py", line 735, in app await route.handle(scope, receive, send) File ".../.venv/lib/python3.12/site-packages/starlette/routing.py", line 460, in handle await self.app(scope, receive, send) File ".../.venv/lib/python3.12/site-packages/fastapi/applications.py", line 1054, in __call__ await super().__call__(scope, receive, send) File ".../.venv/lib/python3.12/site-packages/starlette/applications.py", line 113, in __call__ await self.middleware_stack(scope, receive, send) File ".../.venv/lib/python3.12/site-packages/starlette/middleware/errors.py", line 187, in __call__ raise exc File ".../.venv/lib/python3.12/site-packages/starlette/middleware/errors.py", line 165, in __call__ await self.app(scope, receive, _send) File ".../.venv/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 62, in __call__ await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) File ".../.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app raise exc File ".../.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app await app(scope, receive, sender) File ".../.venv/lib/python3.12/site-packages/starlette/routing.py", line 715, in __call__ await self.middleware_stack(scope, receive, send) File ".../.venv/lib/python3.12/site-packages/starlette/routing.py", line 735, in app await route.handle(scope, receive, send) File ".../.venv/lib/python3.12/site-packages/starlette/routing.py", line 288, in handle await self.app(scope, receive, send) File ".../.venv/lib/python3.12/site-packages/starlette/routing.py", line 76, in app await wrap_app_handling_exceptions(app, request)(scope, receive, send) File ".../.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app raise exc File ".../.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app await app(scope, receive, sender) File ".../.venv/lib/python3.12/site-packages/starlette/routing.py", line 73, in app response = await f(request) ^^^^^^^^^^^^^^^^ File ".../.venv/lib/python3.12/site-packages/fastapi/applications.py", line 1009, in openapi return JSONResponse(self.openapi()) ^^^^^^^^^^^^^^ File ".../.venv/lib/python3.12/site-packages/fastapi/applications.py", line 981, in openapi self.openapi_schema = get_openapi( ^^^^^^^^^^^^ File ".../.venv/lib/python3.12/site-packages/fastapi/openapi/utils.py", line 493, in get_openapi field_mapping, definitions = get_definitions( ^^^^^^^^^^^^^^^^ File ".../.venv/lib/python3.12/site-packages/fastapi/_compat.py", line 231, in get_definitions field_mapping, definitions = schema_generator.generate_definitions( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ".../.venv/lib/python3.12/site-packages/pydantic/json_schema.py", line 361, in generate_definitions self.generate_inner(schema) File ".../.venv/lib/python3.12/site-packages/pydantic/json_schema.py", line 441, in generate_inner if 'ref' in schema: ^^^^^^^^^^^^^^^ File "<frozen _collections_abc>", line 813, in __contains__ File ".../.venv/lib/python3.12/site-packages/pydantic/_internal/_mock_val_ser.py", line 41, in __getitem__ return self._get_built().__getitem__(key) ^^^^^^^^^^^^^^^^^ File ".../.venv/lib/python3.12/site-packages/pydantic/_internal/_mock_val_ser.py", line 58, in _get_built raise PydanticUserError(self._error_message, code=self._code) pydantic.errors.PydanticUserError: `TypeAdapter[typing.Annotated[heroes.models.HeroPublicWithTeam, FieldInfo(annotation=HeroPublicWithTeam, required=True)]]` is not fully defined; you should define `typing.Annotated[heroes.models.HeroPublicWithTeam, FieldInfo(annotation=HeroPublicWithTeam, required=True)]` and all referenced types, then call `.rebuild()` on the instance.which shows up as Operating SystemLinux, Windows Operating System Details~> neofetch ▗▄▄▄ ▗▄▄▄▄ ▄▄▄▖ pars@margo ▜███▙ ▜███▙ ▟███▛ ---------- ▜███▙ ▜███▙▟███▛ OS: NixOS 25.05pre745949.9189ac18287c (Warbler) x86_64 ▜███▙ ▜██████▛ Host: Framework FRANMECP02 ▟█████████████████▙ ▜████▛ ▟▙ Kernel: 6.12.5 ▟███████████████████▙ ▜███▙ ▟██▙ Uptime: 41 days, 7 hours, 3 mins ▄▄▄▄▖ ▜███▙ ▟███▛ Packages: 1732 (nix-system), 5302 (nix-user) ▟███▛ ▜██▛ ▟███▛ Shell: fish 3.7.1 ▟███▛ ▜▛ ▟███▛ Resolution: 2256x1504, 1920x1080 ▟███████████▛ ▟██████████▙ DE: none+i3 ▜██████████▛ ▟███████████▛ WM: i3 ▟███▛ ▟▙ ▟███▛ Terminal: kitty ▟███▛ ▟██▙ ▟███▛ Terminal Font: monospace 16.0 ▟███▛ ▜███▙ ▝▀▀▀▀ CPU: Intel Ultra 5 125H (18) @ 4.500GHz ▜██▛ ▜███▙ ▜██████████████████▛ GPU: Intel Arc Graphics] ▜▛ ▟████▙ ▜████████████████▛ Memory: 40429MiB / 47636MiB ▟██████▙ ▜███▙ ▟███▛▜███▙ ▜███▙ ▟███▛ ▜███▙ ▜███▙ ▝▀▀▀ ▀▀▀▀▘ ▀▀▀▘Also tested on a Win11 machine with Ubuntu WSL with the same result. SQLModel Version0.0.22 Python Version3.12.8 Additional ContextNo response |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 3 replies
-
| Adding relationship in the public interface seems to work: ... class HeroPublicWithTeam(HeroPublic): team: "TeamPublic | None" = Relationship()... class TeamPublicWithHeroes(TeamPublic): heroes: List["HeroPublic"] = Relationship()Side note: You do not need |
Beta Was this translation helpful? Give feedback.
-
| You have circular references and to resolve it you need to rebuild your models. Add the following to from heroes.models import HeroPublicWithTeam, HeroPublic from teams.models import TeamPublicWithHeroes, TeamPublic HeroPublicWithTeam.model_rebuild() TeamPublicWithHeroes.model_rebuild() |
Beta Was this translation helpful? Give feedback.

You have circular references and to resolve it you need to rebuild your models.
Add the following to
__init__.py: