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 2 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
: