Skip to content

Commit 9f7e40d

Browse files
authored
Add requestBodies scope support for OpenAPI (#2716)
* Add requestBodies scope support for OpenAPI * fix: improve requestBodies schema parsing by skipping empty schemas
1 parent f4b074f commit 9f7e40d

File tree

9 files changed

+211
-0
lines changed

9 files changed

+211
-0
lines changed

src/datamodel_code_generator/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ class OpenAPIScope(Enum):
243243
Tags = "tags"
244244
Parameters = "parameters"
245245
Webhooks = "webhooks"
246+
RequestBodies = "requestbodies"
246247

247248

248249
class AllExportsScope(Enum):

src/datamodel_code_generator/parser/openapi.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,29 @@ def parse_raw(self) -> None:
819819
webhooks: dict[str, dict[str, Any]] = specification.get("webhooks", {})
820820
self._process_path_items(webhooks, path_parts, "webhooks", [], security, strip_leading_slash=False)
821821

822+
if OpenAPIScope.RequestBodies in self.open_api_scopes:
823+
request_bodies: dict[str, Any] = specification.get("components", {}).get("requestBodies", {})
824+
for body_name, raw_body in request_bodies.items():
825+
resolved_body = self.get_ref_model(raw_body["$ref"]) if "$ref" in raw_body else raw_body
826+
content = resolved_body.get("content", {})
827+
for media_type, media_obj in content.items():
828+
schema = media_obj.get("schema")
829+
if not schema:
830+
continue
831+
self.parse_raw_obj(
832+
body_name,
833+
schema,
834+
[
835+
*path_parts,
836+
"#/components",
837+
"requestBodies",
838+
body_name,
839+
"content",
840+
media_type,
841+
"schema",
842+
],
843+
)
844+
822845
self._resolve_unparsed_json_pointer()
823846

824847
def _collect_discriminator_schemas(self) -> None:
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# generated by datamodel-codegen:
2+
# filename: request_bodies_scope.yaml
3+
# timestamp: 1985-10-26T08:21:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from pydantic import BaseModel, RootModel
8+
9+
10+
class CreatePet(BaseModel):
11+
name: str | None = None
12+
age: int | None = None
13+
14+
15+
class PetUpdate(BaseModel):
16+
name: str | None = None
17+
18+
19+
class UpdatePet(RootModel[PetUpdate]):
20+
root: PetUpdate
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# generated by datamodel-codegen:
2+
# filename: request_bodies_scope_with_ref.yaml
3+
# timestamp: 1985-10-26T08:21:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from pydantic import BaseModel
8+
9+
10+
class CreatePet(BaseModel):
11+
name: str | None = None
12+
13+
14+
class BasePet(BaseModel):
15+
name: str | None = None
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
openapi: "3.0.0"
2+
info:
3+
version: 1.0.0
4+
title: Test API for RequestBodies Scope
5+
paths: {}
6+
components:
7+
requestBodies:
8+
CreatePet:
9+
description: Request body for creating a pet
10+
required: true
11+
content:
12+
application/json:
13+
schema:
14+
type: object
15+
properties:
16+
name:
17+
type: string
18+
age:
19+
type: integer
20+
UpdatePet:
21+
description: Request body for updating a pet
22+
content:
23+
application/json:
24+
schema:
25+
$ref: '#/components/schemas/PetUpdate'
26+
EmptyContent:
27+
description: Request body with no schema
28+
content:
29+
application/json: {}
30+
schemas:
31+
PetUpdate:
32+
type: object
33+
properties:
34+
name:
35+
type: string
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
openapi: "3.0.0"
2+
info:
3+
version: 1.0.0
4+
title: Test API with empty requestBodies
5+
paths: {}
6+
components:
7+
requestBodies: {}
8+
schemas:
9+
Pet:
10+
type: object
11+
properties:
12+
name:
13+
type: string
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
openapi: "3.0.0"
2+
info:
3+
version: 1.0.0
4+
title: Test API for RequestBodies with $ref
5+
paths: {}
6+
components:
7+
requestBodies:
8+
CreatePet:
9+
$ref: '#/components/requestBodies/BasePet'
10+
BasePet:
11+
description: Base pet request body
12+
required: true
13+
content:
14+
application/json:
15+
schema:
16+
type: object
17+
properties:
18+
name:
19+
type: string

tests/main/openapi/test_main_openapi.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4291,3 +4291,29 @@ def test_main_openapi_use_status_code_in_response_name(output_file: Path) -> Non
42914291
expected_file="use_status_code_in_response_name.py",
42924292
extra_args=["--use-status-code-in-response-name", "--openapi-scopes", "schemas", "paths"],
42934293
)
4294+
4295+
4296+
@freeze_time(TIMESTAMP)
4297+
def test_main_openapi_request_bodies_scope(output_file: Path) -> None:
4298+
"""Test generating models from components/requestBodies using requestbodies scope."""
4299+
run_main_and_assert(
4300+
input_path=OPEN_API_DATA_PATH / "request_bodies_scope.yaml",
4301+
output_path=output_file,
4302+
input_file_type="openapi",
4303+
assert_func=assert_file_content,
4304+
expected_file="request_bodies_scope.py",
4305+
extra_args=["--openapi-scopes", "requestbodies", "--output-model-type", "pydantic_v2.BaseModel"],
4306+
)
4307+
4308+
4309+
@freeze_time(TIMESTAMP)
4310+
def test_main_openapi_request_bodies_scope_with_ref(output_file: Path) -> None:
4311+
"""Test generating models from components/requestBodies with $ref at requestBody level."""
4312+
run_main_and_assert(
4313+
input_path=OPEN_API_DATA_PATH / "request_bodies_scope_with_ref.yaml",
4314+
output_path=output_file,
4315+
input_file_type="openapi",
4316+
assert_func=assert_file_content,
4317+
expected_file="request_bodies_scope_with_ref.py",
4318+
extra_args=["--openapi-scopes", "requestbodies", "--output-model-type", "pydantic_v2.BaseModel"],
4319+
)

tests/parser/test_openapi.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,3 +975,62 @@ def test_parse_all_parameters_strict_nullable() -> None:
975975
assert len(fields) == 2
976976
assert fields[0].nullable is True
977977
assert fields[1].nullable is False
978+
979+
980+
def test_openapi_parser_with_request_bodies_scope() -> None:
981+
"""Test parsing OpenAPI with requestBodies scope generates models from components/requestBodies."""
982+
parser = OpenAPIParser(
983+
data_model_field_type=DataModelFieldBase,
984+
source=Path(DATA_PATH / "request_bodies_scope.yaml"),
985+
openapi_scopes=[OpenAPIScope.RequestBodies],
986+
)
987+
result = parser.parse()
988+
assert "CreatePet" in result
989+
assert "name: Optional[str]" in result
990+
assert "age: Optional[int]" in result
991+
992+
993+
def test_openapi_parser_with_request_bodies_scope_ref() -> None:
994+
"""Test parsing OpenAPI with requestBodies scope handles $ref in schema."""
995+
parser = OpenAPIParser(
996+
data_model_field_type=DataModelFieldBase,
997+
source=Path(DATA_PATH / "request_bodies_scope.yaml"),
998+
openapi_scopes=[OpenAPIScope.RequestBodies, OpenAPIScope.Schemas],
999+
)
1000+
result = parser.parse()
1001+
assert "UpdatePet" in result
1002+
assert "PetUpdate" in result
1003+
1004+
1005+
def test_openapi_parser_with_request_bodies_scope_empty() -> None:
1006+
"""Test parsing OpenAPI with requestBodies scope when requestBodies is empty."""
1007+
parser = OpenAPIParser(
1008+
data_model_field_type=DataModelFieldBase,
1009+
source=Path(DATA_PATH / "request_bodies_scope_empty.yaml"),
1010+
openapi_scopes=[OpenAPIScope.RequestBodies],
1011+
)
1012+
result = parser.parse()
1013+
assert result in ({}, "")
1014+
1015+
1016+
def test_openapi_parser_with_request_bodies_scope_no_schema() -> None:
1017+
"""Test parsing OpenAPI with requestBodies scope skips content without schema."""
1018+
parser = OpenAPIParser(
1019+
data_model_field_type=DataModelFieldBase,
1020+
source=Path(DATA_PATH / "request_bodies_scope.yaml"),
1021+
openapi_scopes=[OpenAPIScope.RequestBodies],
1022+
)
1023+
result = parser.parse()
1024+
assert "EmptyContent" not in result
1025+
1026+
1027+
def test_openapi_parser_with_request_bodies_scope_body_ref() -> None:
1028+
"""Test parsing OpenAPI with requestBodies scope handles $ref at requestBody level."""
1029+
parser = OpenAPIParser(
1030+
data_model_field_type=DataModelFieldBase,
1031+
source=Path(DATA_PATH / "request_bodies_scope_with_ref.yaml"),
1032+
openapi_scopes=[OpenAPIScope.RequestBodies],
1033+
)
1034+
result = parser.parse()
1035+
assert "CreatePet" in result or "BasePet" in result
1036+
assert "name: Optional[str]" in result

0 commit comments

Comments
 (0)