Skip to content

Commit 5f6ff0b

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 4a2f3db commit 5f6ff0b

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
@@ -75,6 +75,10 @@ def build(
7575
else:
7676
class_string = title
7777
class_info = Class.from_string(string=class_string, config=config)
78+
if class_info.name in schemas.classes_by_name and data.title and name:
79+
fallback_class_info = Class.from_string(string=name, config=config)
80+
if fallback_class_info.name not in schemas.classes_by_name:
81+
class_info = fallback_class_info
7882
model_roots = {*roots, class_info.name}
7983
required_properties: list[Property] | None = None
8084
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
@@ -167,6 +167,23 @@ def test_model_name_conflict(self, config):
167167
assert new_schemas == schemas
168168
assert err == PropertyError(detail='Attempted to generate duplicate models with name "OtherModel"', data=data)
169169

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

0 commit comments

Comments
 (0)