Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/datamodel_code_generator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ class OpenAPIScope(Enum):
Tags = "tags"
Parameters = "parameters"
Webhooks = "webhooks"
RequestBodies = "requestbodies"


class AllExportsScope(Enum):
Expand Down
23 changes: 23 additions & 0 deletions src/datamodel_code_generator/parser/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,29 @@ def parse_raw(self) -> None:
webhooks: dict[str, dict[str, Any]] = specification.get("webhooks", {})
self._process_path_items(webhooks, path_parts, "webhooks", [], security, strip_leading_slash=False)

if OpenAPIScope.RequestBodies in self.open_api_scopes:
request_bodies: dict[str, Any] = specification.get("components", {}).get("requestBodies", {})
for body_name, raw_body in request_bodies.items():
resolved_body = self.get_ref_model(raw_body["$ref"]) if "$ref" in raw_body else raw_body
content = resolved_body.get("content", {})
for media_type, media_obj in content.items():
schema = media_obj.get("schema")
if not schema:
continue
self.parse_raw_obj(
body_name,
schema,
[
*path_parts,
"#/components",
"requestBodies",
body_name,
"content",
media_type,
"schema",
],
)

self._resolve_unparsed_json_pointer()

def _collect_discriminator_schemas(self) -> None:
Expand Down
20 changes: 20 additions & 0 deletions tests/data/expected/main/openapi/request_bodies_scope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# generated by datamodel-codegen:
# filename: request_bodies_scope.yaml
# timestamp: 1985-10-26T08:21:00+00:00

from __future__ import annotations

from pydantic import BaseModel, RootModel


class CreatePet(BaseModel):
name: str | None = None
age: int | None = None


class PetUpdate(BaseModel):
name: str | None = None


class UpdatePet(RootModel[PetUpdate]):
root: PetUpdate
15 changes: 15 additions & 0 deletions tests/data/expected/main/openapi/request_bodies_scope_with_ref.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# generated by datamodel-codegen:
# filename: request_bodies_scope_with_ref.yaml
# timestamp: 1985-10-26T08:21:00+00:00

from __future__ import annotations

from pydantic import BaseModel


class CreatePet(BaseModel):
name: str | None = None


class BasePet(BaseModel):
name: str | None = None
35 changes: 35 additions & 0 deletions tests/data/openapi/request_bodies_scope.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Test API for RequestBodies Scope
paths: {}
components:
requestBodies:
CreatePet:
description: Request body for creating a pet
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
age:
type: integer
UpdatePet:
description: Request body for updating a pet
content:
application/json:
schema:
$ref: '#/components/schemas/PetUpdate'
EmptyContent:
description: Request body with no schema
content:
application/json: {}
schemas:
PetUpdate:
type: object
properties:
name:
type: string
13 changes: 13 additions & 0 deletions tests/data/openapi/request_bodies_scope_empty.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Test API with empty requestBodies
paths: {}
components:
requestBodies: {}
schemas:
Pet:
type: object
properties:
name:
type: string
19 changes: 19 additions & 0 deletions tests/data/openapi/request_bodies_scope_with_ref.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Test API for RequestBodies with $ref
paths: {}
components:
requestBodies:
CreatePet:
$ref: '#/components/requestBodies/BasePet'
BasePet:
description: Base pet request body
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
26 changes: 26 additions & 0 deletions tests/main/openapi/test_main_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -4291,3 +4291,29 @@ def test_main_openapi_use_status_code_in_response_name(output_file: Path) -> Non
expected_file="use_status_code_in_response_name.py",
extra_args=["--use-status-code-in-response-name", "--openapi-scopes", "schemas", "paths"],
)


@freeze_time(TIMESTAMP)
def test_main_openapi_request_bodies_scope(output_file: Path) -> None:
"""Test generating models from components/requestBodies using requestbodies scope."""
run_main_and_assert(
input_path=OPEN_API_DATA_PATH / "request_bodies_scope.yaml",
output_path=output_file,
input_file_type="openapi",
assert_func=assert_file_content,
expected_file="request_bodies_scope.py",
extra_args=["--openapi-scopes", "requestbodies", "--output-model-type", "pydantic_v2.BaseModel"],
)


@freeze_time(TIMESTAMP)
def test_main_openapi_request_bodies_scope_with_ref(output_file: Path) -> None:
"""Test generating models from components/requestBodies with $ref at requestBody level."""
run_main_and_assert(
input_path=OPEN_API_DATA_PATH / "request_bodies_scope_with_ref.yaml",
output_path=output_file,
input_file_type="openapi",
assert_func=assert_file_content,
expected_file="request_bodies_scope_with_ref.py",
extra_args=["--openapi-scopes", "requestbodies", "--output-model-type", "pydantic_v2.BaseModel"],
)
59 changes: 59 additions & 0 deletions tests/parser/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -975,3 +975,62 @@ def test_parse_all_parameters_strict_nullable() -> None:
assert len(fields) == 2
assert fields[0].nullable is True
assert fields[1].nullable is False


def test_openapi_parser_with_request_bodies_scope() -> None:
"""Test parsing OpenAPI with requestBodies scope generates models from components/requestBodies."""
parser = OpenAPIParser(
data_model_field_type=DataModelFieldBase,
source=Path(DATA_PATH / "request_bodies_scope.yaml"),
openapi_scopes=[OpenAPIScope.RequestBodies],
)
result = parser.parse()
assert "CreatePet" in result
assert "name: Optional[str]" in result
assert "age: Optional[int]" in result


def test_openapi_parser_with_request_bodies_scope_ref() -> None:
"""Test parsing OpenAPI with requestBodies scope handles $ref in schema."""
parser = OpenAPIParser(
data_model_field_type=DataModelFieldBase,
source=Path(DATA_PATH / "request_bodies_scope.yaml"),
openapi_scopes=[OpenAPIScope.RequestBodies, OpenAPIScope.Schemas],
)
result = parser.parse()
assert "UpdatePet" in result
assert "PetUpdate" in result


def test_openapi_parser_with_request_bodies_scope_empty() -> None:
"""Test parsing OpenAPI with requestBodies scope when requestBodies is empty."""
parser = OpenAPIParser(
data_model_field_type=DataModelFieldBase,
source=Path(DATA_PATH / "request_bodies_scope_empty.yaml"),
openapi_scopes=[OpenAPIScope.RequestBodies],
)
result = parser.parse()
assert result in ({}, "")


def test_openapi_parser_with_request_bodies_scope_no_schema() -> None:
"""Test parsing OpenAPI with requestBodies scope skips content without schema."""
parser = OpenAPIParser(
data_model_field_type=DataModelFieldBase,
source=Path(DATA_PATH / "request_bodies_scope.yaml"),
openapi_scopes=[OpenAPIScope.RequestBodies],
)
result = parser.parse()
assert "EmptyContent" not in result


def test_openapi_parser_with_request_bodies_scope_body_ref() -> None:
"""Test parsing OpenAPI with requestBodies scope handles $ref at requestBody level."""
parser = OpenAPIParser(
data_model_field_type=DataModelFieldBase,
source=Path(DATA_PATH / "request_bodies_scope_with_ref.yaml"),
openapi_scopes=[OpenAPIScope.RequestBodies],
)
result = parser.parse()
assert "CreatePet" in result or "BasePet" in result
assert "name: Optional[str]" in result
Loading