mirror of https://github.com/tiangolo/fastapi.git
Fix non-deterministic openapi spec
v0.128.0 introduced non-deterministic openapi spec schema naming in the case of multiple pydantic models with the same name. Commit [e300630](e300630551 (diff-1086603fdd56511aafd1d279396b142b803e48164327148e6de26cef4cdaed81L504)) removed the check for conflicting names. The component names two classes named `User` would randomly be either ``` User tests__test_duplicate_model_names_openapi__b__model__User ``` ``` tests__test_duplicate_model_names_openapi__a__model__User User ``` With this change, we reintroduce the conflict check to always use fully qualified names in case of conflict.
This commit is contained in:
parent
07f08fc79a
commit
44125260cf
|
|
@ -491,11 +491,25 @@ def normalize_name(name: str) -> str:
|
|||
return re.sub(r"[^a-zA-Z0-9.\-_]", "_", name)
|
||||
|
||||
|
||||
def get_long_model_name(model: TypeModelOrEnum) -> str:
|
||||
return f"{model.__module__}__{model.__qualname__}".replace(".", "__")
|
||||
|
||||
|
||||
def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str]:
|
||||
name_model_map = {}
|
||||
conflicting_names: set[str] = set()
|
||||
for model in unique_models:
|
||||
model_name = normalize_name(model.__name__)
|
||||
name_model_map[model_name] = model
|
||||
if model_name in conflicting_names:
|
||||
model_name = get_long_model_name(model)
|
||||
name_model_map[model_name] = model
|
||||
elif model_name in name_model_map:
|
||||
conflicting_names.add(model_name)
|
||||
conflicting_model = name_model_map.pop(model_name)
|
||||
name_model_map[get_long_model_name(conflicting_model)] = conflicting_model
|
||||
name_model_map[get_long_model_name(model)] = model
|
||||
else:
|
||||
name_model_map[model_name] = model
|
||||
return {v: k for k, v in name_model_map.items()}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
a: int
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
from fastapi import FastAPI
|
||||
|
||||
from tests.test_duplicate_model_names_openapi.a.model import User as UserA
|
||||
from tests.test_duplicate_model_names_openapi.b.model import User as UserB
|
||||
from tests.test_duplicate_model_names_openapi.c.model import User as UserC
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/a", response_model=UserA)
|
||||
def user_a():
|
||||
...
|
||||
|
||||
|
||||
@app.get("/b", response_model=UserB)
|
||||
def user_b():
|
||||
...
|
||||
|
||||
|
||||
@app.get("/c", response_model=UserC)
|
||||
def user_c():
|
||||
...
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
b: int
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
c: int
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from .app import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
schema = response.json()
|
||||
component_names = schema["components"]["schemas"].keys()
|
||||
|
||||
# Fully qualified names due to conflict
|
||||
assert set(component_names) == {
|
||||
"tests__test_duplicate_model_names_openapi__a__model__User",
|
||||
"tests__test_duplicate_model_names_openapi__b__model__User",
|
||||
"tests__test_duplicate_model_names_openapi__c__model__User",
|
||||
}
|
||||
|
||||
assert schema == {
|
||||
"openapi": "3.1.0",
|
||||
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/a": {
|
||||
"get": {
|
||||
"summary": "User A",
|
||||
"operationId": "user_a_a_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/tests__test_duplicate_model_names_openapi__a__model__User"
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"/b": {
|
||||
"get": {
|
||||
"summary": "User B",
|
||||
"operationId": "user_b_b_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/tests__test_duplicate_model_names_openapi__b__model__User"
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"/c": {
|
||||
"get": {
|
||||
"summary": "User C",
|
||||
"operationId": "user_c_c_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/tests__test_duplicate_model_names_openapi__c__model__User"
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"tests__test_duplicate_model_names_openapi__a__model__User": {
|
||||
"properties": {"a": {"type": "integer", "title": "A"}},
|
||||
"type": "object",
|
||||
"required": ["a"],
|
||||
"title": "User",
|
||||
},
|
||||
"tests__test_duplicate_model_names_openapi__b__model__User": {
|
||||
"properties": {"b": {"type": "integer", "title": "B"}},
|
||||
"type": "object",
|
||||
"required": ["b"],
|
||||
"title": "User",
|
||||
},
|
||||
"tests__test_duplicate_model_names_openapi__c__model__User": {
|
||||
"properties": {"c": {"type": "integer", "title": "C"}},
|
||||
"type": "object",
|
||||
"required": ["c"],
|
||||
"title": "User",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
Loading…
Reference in New Issue