Non-deterministic Open API Spec Generation #14376
-
First Check
Commit to Help
Example Code# routers/one.py
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter(prefix="/one")
class Data(BaseModel):
state: str
class Response(BaseModel):
data: Data
@router.get(
"",
response_model=Response,
)
def get__one():
pass
#routers/two.py
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter(prefix="/two")
class Data(BaseModel):
state: str
class Response(BaseModel):
data: Data
@router.get(
"",
response_model=Response,
)
def get__two():
pass
#app.py
from fastapi import FastAPI
from routers import one, two
app = FastAPI()
app.include_router(one.router)
app.include_router(two.router)
#script.py
from pathlib import Path
import json
from app import app
def generate(check: bool = False):
path = Path("openapi.json")
openapi_schema = app.openapi()
with path.open("w") as f:
json.dumps(f, indent=2)
if __name__ == "__main__":
generate()DescriptionLines 304 to 313 in 456008a We are generating the openapi spec similar to the MRE above. When the JSON Schema is generated any models with a shared schema will have a shared ref. In the example above the definitions refs before remapping look like As these are remapped, the old name The field-mapping here could be sorted which would at least make it deterministic, but I appreciate this is a little fiddly so I thought I would just open a discussion here? In the mean-time we are adding docstrings to affected models to disambiguate their json-schema. Operating SystemmacOS Operating System DetailsFastAPI Version0.121.3 Pydantic Version2.11.1 Python Version3.11.11 Additional ContextNo response |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 3 replies
-
|
Looks similar to: #14177 (comment) Why don't you:
? Just making this deterministic again doesn't look like perfect solution. I think we should at least warn that duplicated schema names are used |
Beta Was this translation helpful? Give feedback.
-
|
hey @mattalexanderhill, OpenAPI spec generation should be deterministic. You are correct on that. Even though modern Python preserves insertion order, reliance on dictionary iteration order during complex recursive schema mapping is fragile and frequently breaks CI pipelines like yours. Since you have already identified the loop causing the non-determinism ( The issue is triggered because FastAPI detects a naming collision ( To unblock your CI immediately, the most robust fix is to bypass that collision logic entirely by ensuring the classes have unique names at the Python class definition level: # routers/one.py
class OneResponse(BaseModel): # Renamed from Response
data: Data
# routers/two.py
class TwoResponse(BaseModel): # Renamed from Response
data: DataI know this is similar to "Option 2" mentioned above, but it really is the only way to guarantee stability until the framework patch lands. Using If you find this breakdown helpful for your debugging, please mark this response as the answer so others with similar OpenAPI spec generation needs can find it easily! |
Beta Was this translation helpful? Give feedback.
hey @mattalexanderhill,
OpenAPI spec generation should be deterministic. You are correct on that. Even though modern Python preserves insertion order, reliance on dictionary iteration order during complex recursive schema mapping is fragile and frequently breaks CI pipelines like yours.
Since you have already identified the loop causing the non-determinism (
field_mapping.items()), I strongly encourage you to submit a PR to sort it.Simply changing the iteration to
sorted(field_mapping.items(), key=lambda x: x[0].__name__)(or a similar stable key) would likely solve this for the whole community. It is a valid bug that the spec generation fluctuates.The issue is triggered because FastAPI …