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 a hash of the fully qualified names in case of conflict.
This commit is contained in:
parent
608ff552ba
commit
748840e77f
|
|
@ -1,3 +1,4 @@
|
||||||
|
import hashlib
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
@ -491,11 +492,25 @@ def normalize_name(name: str) -> str:
|
||||||
return re.sub(r"[^a-zA-Z0-9.\-_]", "_", name)
|
return re.sub(r"[^a-zA-Z0-9.\-_]", "_", name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_long_model_name(model: TypeModelOrEnum) -> str:
|
||||||
|
return f"{model.__qualname__}_{hashlib.sha1(model.__module__.encode()).hexdigest()}"
|
||||||
|
|
||||||
|
|
||||||
def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str]:
|
def get_model_name_map(unique_models: TypeModelSet) -> dict[TypeModelOrEnum, str]:
|
||||||
name_model_map = {}
|
name_model_map = {}
|
||||||
|
conflicting_names: set[str] = set()
|
||||||
for model in unique_models:
|
for model in unique_models:
|
||||||
model_name = normalize_name(model.__name__)
|
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()}
|
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,19 @@
|
||||||
|
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,105 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Hashed fully qualified names due to conflict
|
||||||
|
assert set(component_names) == {
|
||||||
|
"User_46cbb9d5ba5400ba253a3caa105ccc482d1388ff",
|
||||||
|
"User_42908c96f764fbf5360c1ae9d3a2a96d90d97bef",
|
||||||
|
"User_3c4b81c0ed34bf97eb9b4ac8ced9dc17e0dccbb7",
|
||||||
|
}
|
||||||
|
|
||||||
|
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/User_42908c96f764fbf5360c1ae9d3a2a96d90d97bef"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/b": {
|
||||||
|
"get": {
|
||||||
|
"summary": "User B",
|
||||||
|
"operationId": "user_b_b_get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/User_3c4b81c0ed34bf97eb9b4ac8ced9dc17e0dccbb7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/c": {
|
||||||
|
"get": {
|
||||||
|
"summary": "User C",
|
||||||
|
"operationId": "user_c_c_get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/User_46cbb9d5ba5400ba253a3caa105ccc482d1388ff"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"User_3c4b81c0ed34bf97eb9b4ac8ced9dc17e0dccbb7": {
|
||||||
|
"properties": {"b": {"type": "integer", "title": "B"}},
|
||||||
|
"type": "object",
|
||||||
|
"required": ["b"],
|
||||||
|
"title": "User",
|
||||||
|
},
|
||||||
|
"User_42908c96f764fbf5360c1ae9d3a2a96d90d97bef": {
|
||||||
|
"properties": {"a": {"type": "integer", "title": "A"}},
|
||||||
|
"type": "object",
|
||||||
|
"required": ["a"],
|
||||||
|
"title": "User",
|
||||||
|
},
|
||||||
|
"User_46cbb9d5ba5400ba253a3caa105ccc482d1388ff": {
|
||||||
|
"properties": {"c": {"type": "integer", "title": "C"}},
|
||||||
|
"type": "object",
|
||||||
|
"required": ["c"],
|
||||||
|
"title": "User",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue