When working with modular imports with FastAPI and SQLModel, I am getting the following error if I open /docs:
TypeError: issubclass() arg 1 must be a class
Here is a reproducible example.
user.py
from typing import List, TYPE_CHECKING, Optional
from sqlmodel import SQLModel, Field
if TYPE_CHECKING:
from item import Item
class User(SQLModel):
id: int = Field(default=None, primary_key=True)
age: Optional[int]
bought_items: List["Item"] = []
item.py
from sqlmodel import SQLModel, Field
class Item(SQLModel):
id: int = Field(default=None, primary_key=True)
price: float
name: str
main.py
from fastapi import FastAPI
from user import User
app = FastAPI()
@app.get("/", response_model=User)
def main():
return {"message": "working just fine"}
I followed along the tutorial from sqlmodel https://sqlmodel.tiangolo.com/tutorial/code-structure/#make-circular-imports-work. If I would put the models in the same file, it all works fine. As my actual models are quite complex, I need to rely on the modular imports though.
Traceback:
Traceback (most recent call last):
File "/Users/felix/opt/anaconda3/envs/fastapi_test/lib/python3.10/site-packages/fastapi/utils.py", line 45, in get_model_definitions
m_schema, m_definitions, m_nested_models = model_process_schema(
File "pydantic/schema.py", line 580, in pydantic.schema.model_process_schema
File "pydantic/schema.py", line 621, in pydantic.schema.model_type_schema
File "pydantic/schema.py", line 254, in pydantic.schema.field_schema
File "pydantic/schema.py", line 461, in pydantic.schema.field_type_schema
File "pydantic/schema.py", line 847, in pydantic.schema.field_singleton_schema
File "pydantic/schema.py", line 698, in pydantic.schema.field_singleton_sub_fields_schema
File "pydantic/schema.py", line 526, in pydantic.schema.field_type_schema
File "pydantic/schema.py", line 921, in pydantic.schema.field_singleton_schema
File "/Users/felix/opt/anaconda3/envs/fastapi_test/lib/python3.10/abc.py", line 123, in __subclasscheck__
return _abc_subclasscheck(cls, subclass)
TypeError: issubclass() arg 1 must be a class
You need to call User.update_forward_refs(Item=Item)
before the OpenAPI setup.
So, this is actually quite a bit trickier and I am not quite sure yet, why this is not mentioned in the docs. Maybe I am missing something. Anyway...
If you follow the traceback, you'll see that the error occurs because in line 921 of pydantic.schema
in the field_singleton_schema
function a check is performed to see if issubclass(field_type, BaseModel)
and at that point field_type
is not in fact a type
instance.
A bit of debugging reveals that this occurs, when the schema for the User
model is being generated and the bought_items
field is being processed. At that point the annotation is processed and the type argument for List
is still a forward reference to Item
. Meaning it is not the actual Item
class itself. And that is what is passed to issubclass
and causes the error.
This is a fairly common problem, when dealing with recursive or circular relationships between Pydantic models, which is why they were so kind to provide a special method just for that. It is explained in the Postponed annotations section of the documentation. The method is update_forward_refs
and as the name suggests, it is there to resolve forward references.
What is tricky in this case, is that you need to provide it with an updated namespace to resolve the Item
reference. To do that you need to actually have the real Item
class in scope because that is what needs to be in that namespace. Where you do it does not really matter. You could for example import User
model into your item
module and call it there (obviously below the definition of Item
):
from sqlmodel import SQLModel, Field
from .user import User
class Item(SQLModel):
id: int = Field(default=None, primary_key=True)
price: float
name: str
User.update_forward_refs(Item=Item)
But that call needs to happen before an attempt is made to set up that schema. Thus you'll at least need to import the item
module in your main
module:
from fastapi import FastAPI
from .user import User
from . import item
api = FastAPI()
@api.get("/", response_model=User)
def main():
return {"message": "working just fine"}
At that point it is probably simpler to have a sub-package with just the model modules and import all of them in the __init__.py
of that sub-package.
The reason I gave the example of putting the User.update_forward_refs
call in below your Item
definition is that these situations typically occur, when you actually have a circular relationship, i.e. if your Item
class had a users
field for example, which was typed as list[User]
. Then you'd have to import User
there anyway and might as well just update the references there.
In your specific example, you don't actually have any circular dependencies, so there is strictly speaking no need for the TYPE_CHECKING
escape. You can simply do from .item import Item
inside user.py
and put the actual class in your annotation as bought_items: list[Item]
. But I assume you simplified the actual use case and simply forgot to include the circular dependency.
Maybe I am missing something and someone else here can find a way to call update_forward_refs
without the need to provide Item
explicitly, but this way should definitely work.