Skip to content

Commit 2f3d40a

Browse files
fix: fall back to schema key when two schemas share a title
Tools like FastAPI emit duplicate `title` values for input and output variants of the same model (for example `Thing-Input` and `Thing-Output` both carrying `title: Thing`). The class name is derived from the title, so the second variant collided with the first and was rejected with `Attempted to generate duplicate models`. The generator emitted a warning and silently dropped the schema along with every endpoint that referenced it. When the title-derived class name is already taken, the second variant now falls back to a class name derived from its schema key (`Thing-Output` becomes `ThingOutput`) provided that key is unique. The original schema's name is preserved, and both variants and their endpoints generate. A functional test in `end_to_end_tests/functional_tests` covers the inline spec, and a unit test in `tests/test_parser/test_properties/test_model_property.py` exercises the new branch in `ModelProperty.build`.
1 parent 7939364 commit 2f3d40a

4 files changed

Lines changed: 70 additions & 0 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
default: patch
3+
---
4+
5+
# Fall back to schema key when two schemas share a `title`
6+
7+
Tools like FastAPI emit duplicate `title` values for input and output variants of the same model (for example `Thing-Input` and `Thing-Output` both carrying `title: Thing`). The first variant took the title-derived class name and the second was silently dropped with an `Attempted to generate duplicate models` error, along with every endpoint that referenced it.
8+
9+
The second variant now falls back to a class name derived from its schema key (`Thing-Output` becomes `ThingOutput`), so both schemas survive and their endpoints generate.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from end_to_end_tests.functional_tests.helpers import (
2+
with_generated_client_fixture,
3+
with_generated_code_imports,
4+
)
5+
6+
7+
@with_generated_client_fixture(
8+
"""
9+
components:
10+
schemas:
11+
Thing-Input:
12+
title: Thing
13+
type: object
14+
properties:
15+
name: {"type": "string"}
16+
Thing-Output:
17+
title: Thing
18+
type: object
19+
properties:
20+
name: {"type": "string"}
21+
id: {"type": "string"}
22+
"""
23+
)
24+
@with_generated_code_imports(".models.Thing", ".models.ThingOutput")
25+
class TestCollidingTitlesFallBackToSchemaKey:
26+
"""FastAPI emits the same ``title`` for input and output variants of a model
27+
(for example ``Thing-Input`` and ``Thing-Output`` both carrying ``title: Thing``).
28+
The first variant takes the title-derived class name, and the second falls back
29+
to its schema key so both schemas survive.
30+
"""
31+
32+
def test_first_variant_uses_title(self, Thing):
33+
assert Thing.__name__ == "Thing"
34+
instance = Thing(name="x")
35+
assert instance.to_dict() == {"name": "x"}
36+
37+
def test_second_variant_uses_schema_key(self, ThingOutput):
38+
assert ThingOutput.__name__ == "ThingOutput"
39+
instance = ThingOutput(name="x", id="123")
40+
assert instance.to_dict() == {"name": "x", "id": "123"}

openapi_python_client/parser/properties/model_property.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ def build(
7474
else:
7575
class_string = title
7676
class_info = Class.from_string(string=class_string, config=config)
77+
if class_info.name in schemas.classes_by_name and data.title and name:
78+
fallback_class_info = Class.from_string(string=name, config=config)
79+
if fallback_class_info.name not in schemas.classes_by_name:
80+
class_info = fallback_class_info
7781
model_roots = {*roots, class_info.name}
7882
required_properties: list[Property] | None = None
7983
optional_properties: list[Property] | None = None

tests/test_parser/test_properties/test_model_property.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,23 @@ def test_model_name_conflict(self, config):
164164
assert new_schemas == schemas
165165
assert err == PropertyError(detail='Attempted to generate duplicate models with name "OtherModel"', data=data)
166166

167+
def test_model_name_conflict_fallback(self, config):
168+
data = oai.Schema.model_construct(title="OtherModel")
169+
schemas = Schemas(classes_by_name={"OtherModel": None})
170+
171+
model, _new_schemas = ModelProperty.build(
172+
data=data,
173+
name="UniqueModelName",
174+
schemas=schemas,
175+
required=True,
176+
parent_name=None,
177+
config=config,
178+
roots={"root"},
179+
process_properties=True,
180+
)
181+
182+
assert model.class_info.name == "UniqueModelName"
183+
167184
@pytest.mark.parametrize(
168185
"name, title, parent_name, use_title_prefixing, expected",
169186
ids=(

0 commit comments

Comments
 (0)