10

I would love to use a schema that looks something like the following in FastAPI:

from __future__ import annotations
from typing import List
from pydantic import BaseModel


class Project(BaseModel):
    members: List[User]


class User(BaseModel):
    projects: List[Project]


Project.update_forward_refs()

but in order to keep my project structure clean, I would ofc. like to define these in separate files. How could I do this without creating a circular reference?

With the code above the schema generation in FastAPI works fine, I just dont know how to separate it out into separate files. In a later step I would then instead of using attributes use @propertys to define the getters for these objects in subclasses of them. But for the OpenAPI doc generation, I need this combined - I think.

Nils Ziehn
  • 3,927
  • 5
  • 24
  • 40

3 Answers3

11

There are three cases when circular dependency may work in Python:

  • Top of module: import package.module
  • Bottom of module: from package.module import attribute
  • Top of function: works both

In your situation, the second case "bottom of module" will help. Because you need to use update_forward_refs function to resolve pydantic postponed annotations like this:

# project.py
from typing import List
from pydantic import BaseModel


class Project(BaseModel):
    members: "List[User]"


from user import User
Project.update_forward_refs()
# user.py
from typing import List
from pydantic import BaseModel


class User(BaseModel):
    projects: "List[Project]"


from project import Project
User.update_forward_refs()

Nonetheless, I would strongly discourage you from intentionally introducing circular dependencies

alex_noname
  • 17,963
  • 2
  • 40
  • 56
  • Hi alex, thanks for the answer and the input - I agree that I need to find another way how to model this generally! – Nils Ziehn Aug 15 '20 at 12:41
0

If I want to split the models/schema into separate files, I will create extra files for the ProjectBase model and UserBase model so the Project model and User model could inherit from them. I will do like this:

#project_base.py
from pydantic import BaseModel

class ProjectBase(BaseModel):
    id: int
    title: str
    
    class Config:
        orm_mode=True

 

#user_base.py
from pydantic import BaseModel

class UserBase(BaseModel):
    id: int
    title: str
    
    class Config:
        orm_mode=True

 

#project.py
from typing import List
from .project_base import ProjectBase
from .user_base import UserBase

class Project(ProjectBase):
    members: List[UserBase] = []

 

#user.py
from typing import List
from .project_base import ProjectBase
from .user_base import UserBase

class User(UserBase):
projects: List[ProjectBase] = []

note: for this method the orm_mode must be put in the ProjectBase and UserBase, so it can read by Project and User even if it is not a dict

bas_baskara
  • 101
  • 2
  • 4
0

Just put all your schema imports at the bottom of the file, after all classes, and call update_forward_refs().

#1/4
from __future__ import annotations # this is important to have at the top
from pydantic import BaseModel

#2/4
class A(BaseModel):
    my_x: X   # a pydantic schema from another file

class B(BaseModel):
    my_y: Y   # a pydantic schema from another file

class C(BaseModel):
    my_z: int

#3/4
from myapp.schemas.x import X   # related schemas we import after all classes
from myapp.schemas.y import Y

#4/4
A.update_forward_refs()   # tell the system that A has a related pydantic schema
B.update_forward_refs()   # tell the system that B has a related pydantic schema
                          # for C we don't need it, because C has just an integer field.

NOTE: Do this in every file that has schema imports. That will enable you make any combination without circular import problems.

NOTE 2: People usually put the imports and update_forward_refs() after every class, and then report that it doesn't work. That is usually because if an app is complex, you do not know what import is calling which class and when. Therefore, if you put it at the bottom, you are sure that every class will be 'scanned' and visible for others.

elano7
  • 730
  • 1
  • 10
  • 13