Skip to content

Commit 82b28d9

Browse files
Allow including path parameters in generated models (#2445)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent ec84e8e commit 82b28d9

File tree

10 files changed

+160
-3
lines changed

10 files changed

+160
-3
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,9 @@ Template customization:
502502
option (require black 20.8b0 or later)
503503

504504
OpenAPI-only options:
505+
--include-path-parameters
506+
Include path parameters in generated parameter models in addition to
507+
query parameters (Only OpenAPI)
505508
--openapi-scopes {schemas,paths,tags,parameters} [{schemas,paths,tags,parameters} ...]
506509
Scopes of OpenAPI model generation (default: schemas)
507510
--strict-nullable Treat default field as a non-nullable field (Only OpenAPI)

docs/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,9 @@ Template customization:
494494
option (require black 20.8b0 or later)
495495

496496
OpenAPI-only options:
497+
--include-path-parameters
498+
Include path parameters in generated parameter models in addition to
499+
query parameters (Only OpenAPI)
497500
--openapi-scopes {schemas,paths,tags,parameters} [{schemas,paths,tags,parameters} ...]
498501
Scopes of OpenAPI model generation (default: schemas)
499502
--strict-nullable Treat default field as a non-nullable field (Only OpenAPI)

src/datamodel_code_generator/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
258258
field_include_all_keys: bool = False,
259259
field_extra_keys_without_x_prefix: set[str] | None = None,
260260
openapi_scopes: list[OpenAPIScope] | None = None,
261+
include_path_parameters: bool = False,
261262
graphql_scopes: list[GraphQLScope] | None = None, # noqa: ARG001
262263
wrap_string_literal: bool | None = None,
263264
use_title_as_name: bool = False,
@@ -327,6 +328,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
327328

328329
parser_class: type[Parser] = OpenAPIParser
329330
kwargs["openapi_scopes"] = openapi_scopes
331+
kwargs["include_path_parameters"] = include_path_parameters
330332
elif input_file_type == InputFileType.GraphQL:
331333
from datamodel_code_generator.parser.graphql import GraphQLParser # noqa: PLC0415
332334

src/datamodel_code_generator/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ def validate_root(cls, values: Any) -> Any: # noqa: N805
287287
field_include_all_keys: bool = False
288288
field_extra_keys_without_x_prefix: Optional[set[str]] = None # noqa: UP045
289289
openapi_scopes: Optional[list[OpenAPIScope]] = [OpenAPIScope.Schemas] # noqa: UP045
290+
include_path_parameters: bool = False
290291
wrap_string_literal: Optional[bool] = None # noqa: UP045
291292
use_title_as_name: bool = False
292293
use_operation_id_as_name: bool = False
@@ -500,6 +501,7 @@ def main(args: Sequence[str] | None = None) -> Exit: # noqa: PLR0911, PLR0912,
500501
field_include_all_keys=config.field_include_all_keys,
501502
field_extra_keys_without_x_prefix=config.field_extra_keys_without_x_prefix,
502503
openapi_scopes=config.openapi_scopes,
504+
include_path_parameters=config.include_path_parameters,
503505
wrap_string_literal=config.wrap_string_literal,
504506
use_title_as_name=config.use_title_as_name,
505507
use_operation_id_as_name=config.use_operation_id_as_name,

src/datamodel_code_generator/arguments.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,12 @@ def start_section(self, heading: str | None) -> None:
500500
action="store_true",
501501
default=None,
502502
)
503+
openapi_options.add_argument(
504+
"--include-path-parameters",
505+
help="Include path parameters in generated parameter models in addition to query parameters (Only OpenAPI)",
506+
action="store_true",
507+
default=None,
508+
)
503509
openapi_options.add_argument(
504510
"--validation",
505511
help="Deprecated: Enable validation (Only OpenAPI). this option is deprecated. it will be removed in future "

src/datamodel_code_generator/parser/openapi.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ def __init__( # noqa: PLR0913
189189
field_include_all_keys: bool = False,
190190
field_extra_keys_without_x_prefix: set[str] | None = None,
191191
openapi_scopes: list[OpenAPIScope] | None = None,
192+
include_path_parameters: bool = False,
192193
wrap_string_literal: bool | None = False,
193194
use_title_as_name: bool = False,
194195
use_operation_id_as_name: bool = False,
@@ -299,6 +300,7 @@ def __init__( # noqa: PLR0913
299300
parent_scoped_naming=parent_scoped_naming,
300301
)
301302
self.open_api_scopes: list[OpenAPIScope] = openapi_scopes or [OpenAPIScope.Schemas]
303+
self.include_path_parameters: bool = include_path_parameters
302304

303305
def get_ref_model(self, ref: str) -> dict[str, Any]:
304306
ref_file, ref_path = self.model_resolver.resolve_ref(ref).split("#", 1)
@@ -421,8 +423,17 @@ def parse_all_parameters(
421423
for parameter_ in parameters:
422424
parameter = self.resolve_object(parameter_, ParameterObject)
423425
parameter_name = parameter.name
424-
if not parameter_name or parameter.in_ != ParameterLocation.query:
426+
if (
427+
not parameter_name
428+
or parameter.in_ not in {ParameterLocation.query, ParameterLocation.path}
429+
or (parameter.in_ == ParameterLocation.path and not self.include_path_parameters)
430+
):
425431
continue
432+
433+
if any(field.original_name == parameter_name for field in fields):
434+
msg = f"Parameter name '{parameter_name}' is used more than once."
435+
raise Exception(msg) # noqa: TRY002
436+
426437
field_name, alias = self.model_resolver.get_valid_field_name_and_alias(
427438
field_name=parameter_name, excludes=exclude_field_names
428439
)
@@ -518,7 +529,9 @@ def parse_operation(
518529
path_name = operation.operationId
519530
method = ""
520531
self.parse_all_parameters(
521-
self._get_model_name(path_name, method, suffix="ParametersQuery"),
532+
self._get_model_name(
533+
path_name, method, suffix="Parameters" if self.include_path_parameters else "ParametersQuery"
534+
),
522535
operation.parameters,
523536
[*path, "parameters"],
524537
)

tests/data/expected/parser/openapi/openapi_parser_with_query_parameters/output.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class PetForm(BaseModel):
2222
age: Optional[int] = None
2323

2424

25+
class PetsPetIdGetParametersQuery(BaseModel):
26+
include: Optional[str] = None
27+
28+
2529
class Filter(BaseModel):
2630
type: Optional[str] = None
2731
color: Optional[str] = None
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from __future__ import annotations
2+
3+
from enum import Enum
4+
from typing import List, Optional, Union
5+
6+
from pydantic import BaseModel
7+
8+
9+
class Pet(BaseModel):
10+
id: int
11+
name: str
12+
tag: Optional[str] = None
13+
14+
15+
class Error(BaseModel):
16+
code: int
17+
message: str
18+
19+
20+
class PetForm(BaseModel):
21+
name: Optional[str] = None
22+
age: Optional[int] = None
23+
24+
25+
class PetsPetIdGetParameters(BaseModel):
26+
petId: str
27+
include: Optional[str] = None
28+
29+
30+
class Filter(BaseModel):
31+
type: Optional[str] = None
32+
color: Optional[str] = None
33+
34+
35+
class MediaType(Enum):
36+
xml = 'xml'
37+
json = 'json'
38+
39+
40+
class MultipleMediaFilter(BaseModel):
41+
type: Optional[str] = None
42+
media_type: Optional[MediaType] = 'xml'
43+
44+
45+
class MultipleMediaFilter1(BaseModel):
46+
type: Optional[str] = None
47+
media_type: Optional[MediaType] = 'json'
48+
49+
50+
class PetsGetParameters(BaseModel):
51+
limit: Optional[int] = 0
52+
HomeAddress: Optional[str] = 'Unknown'
53+
kind: Optional[str] = 'dog'
54+
filter: Optional[Filter] = None
55+
multipleMediaFilter: Optional[
56+
Union[MultipleMediaFilter, MultipleMediaFilter1]
57+
] = None
58+
59+
60+
class PetsGetResponse(BaseModel):
61+
__root__: List[Pet]
62+
63+
64+
class PetsPostRequest(BaseModel):
65+
name: Optional[str] = None
66+
age: Optional[int] = None

tests/data/openapi/query_parameters.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,30 @@ servers:
1414
security:
1515
- BearerAuth: []
1616
paths:
17+
/pets/{petId}:
18+
get:
19+
summary: Get a pet by ID
20+
operationId: getPet
21+
parameters:
22+
- name: petId
23+
in: path
24+
required: true
25+
description: The pet ID
26+
schema:
27+
type: string
28+
- name: include
29+
in: query
30+
required: false
31+
description: Include additional data
32+
schema:
33+
type: string
34+
responses:
35+
'200':
36+
description: A pet
37+
content:
38+
application/json:
39+
schema:
40+
$ref: "#/components/schemas/Pet"
1741
/pets:
1842
get:
1943
summary: List all pets

tests/parser/test_openapi.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from datamodel_code_generator.model.pydantic import DataModelField
1616
from datamodel_code_generator.parser.base import dump_templates
1717
from datamodel_code_generator.parser.jsonschema import JsonSchemaObject
18-
from datamodel_code_generator.parser.openapi import OpenAPIParser
18+
from datamodel_code_generator.parser.openapi import OpenAPIParser, ParameterObject
1919

2020
DATA_PATH: Path = Path(__file__).parents[1] / "data" / "openapi"
2121

@@ -668,6 +668,40 @@ def test_openapi_parser_with_query_parameters() -> None:
668668
assert parser.parse() == (EXPECTED_OPEN_API_PATH / "openapi_parser_with_query_parameters" / "output.py").read_text()
669669

670670

671+
@pytest.mark.skipif(
672+
black.__version__.split(".")[0] >= "24",
673+
reason="Installed black doesn't support the old style",
674+
)
675+
def test_openapi_parser_with_include_path_parameters() -> None:
676+
parser = OpenAPIParser(
677+
data_model_field_type=DataModelFieldBase,
678+
source=Path(DATA_PATH / "query_parameters.yaml"),
679+
openapi_scopes=[
680+
OpenAPIScope.Parameters,
681+
OpenAPIScope.Schemas,
682+
OpenAPIScope.Paths,
683+
],
684+
include_path_parameters=True,
685+
)
686+
assert (
687+
parser.parse()
688+
== (EXPECTED_OPEN_API_PATH / "openapi_parser_with_query_parameters" / "with_path_params.py").read_text()
689+
)
690+
691+
692+
def test_parse_all_parameters_duplicate_names_exception() -> None:
693+
parser = OpenAPIParser("", include_path_parameters=True)
694+
parameters = [
695+
ParameterObject.parse_obj({"name": "duplicate_param", "in": "path", "schema": {"type": "string"}}),
696+
ParameterObject.parse_obj({"name": "duplicate_param", "in": "query", "schema": {"type": "integer"}}),
697+
]
698+
699+
with pytest.raises(Exception) as exc_info: # noqa: PT011
700+
parser.parse_all_parameters("TestModel", parameters, ["test", "path"])
701+
702+
assert "Parameter name 'duplicate_param' is used more than once." in str(exc_info.value)
703+
704+
671705
@pytest.mark.skipif(
672706
version.parse(pydantic.VERSION) < version.parse("2.9.0"),
673707
reason="Require Pydantic version 2.0.0 or later ",

0 commit comments

Comments
 (0)