fix: Make sure request bodies without explicit names are named 'body'

The `Parameter` class now provides default Python names based on the parameter location when the original name is empty. This prevents parameters from having an empty string as their Python name, especially for request bodies defined without a top-level name.

Close #2213

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 834850255
This commit is contained in:
George Weale
2025-11-20 11:48:46 -08:00
committed by Copybara-Service
parent bf8b85da52
commit 084c2de0da
4 changed files with 76 additions and 7 deletions
@@ -64,11 +64,9 @@ class ApiParameter(BaseModel):
required: bool = False
def model_post_init(self, _: Any):
self.py_name = (
self.py_name
if self.py_name
else rename_python_keywords(_to_snake_case(self.original_name))
)
if not self.py_name:
inferred_name = rename_python_keywords(_to_snake_case(self.original_name))
self.py_name = inferred_name or self._default_py_name()
if isinstance(self.param_schema, str):
self.param_schema = Schema.model_validate_json(self.param_schema)
@@ -77,6 +75,16 @@ class ApiParameter(BaseModel):
self.type_hint = TypeHintHelper.get_type_hint(self.param_schema)
return self
def _default_py_name(self) -> str:
location_defaults = {
'body': 'body',
'query': 'query_param',
'path': 'path_param',
'header': 'header_param',
'cookie': 'cookie_param',
}
return location_defaults.get(self.param_location or '', 'value')
@model_serializer
def _serialize(self):
return {
@@ -139,10 +139,19 @@ class OperationParser:
)
)
else:
# Prefer explicit body name to avoid empty keys when schema lacks type
# information (e.g., oneOf/anyOf/allOf) while retaining legacy behavior
# for simple scalar types.
if schema and (schema.oneOf or schema.anyOf or schema.allOf):
param_name = 'body'
elif not schema or not schema.type:
param_name = 'body'
else:
param_name = ''
self._params.append(
# Empty name for unnamed body param
ApiParameter(
original_name='',
original_name=param_name,
param_location='body',
param_schema=schema,
description=description,
@@ -74,6 +74,24 @@ class TestApiParameter:
)
assert param.py_name == 'param_in'
def test_api_parameter_uses_location_default_when_name_missing(self):
schema = Schema(type='string')
param = ApiParameter(
original_name='',
param_location='body',
param_schema=schema,
)
assert param.py_name == 'body'
def test_api_parameter_uses_value_default_when_location_unknown(self):
schema = Schema(type='integer')
param = ApiParameter(
original_name='',
param_location='',
param_schema=schema,
)
assert param.py_name == 'value'
def test_api_parameter_custom_py_name(self):
schema = Schema(type='integer')
param = ApiParameter(
@@ -164,6 +164,40 @@ def test_process_request_body_no_name():
assert parser._params[0].param_location == 'body'
def test_process_request_body_one_of_schema_assigns_name():
"""Ensures oneOf bodies result in a named parameter."""
operation = Operation(
operationId='one_of_request',
requestBody=RequestBody(
content={
'application/json': MediaType(
schema=Schema(
oneOf=[
Schema(
type='object',
properties={
'type': Schema(type='string'),
'stage': Schema(type='string'),
},
)
],
discriminator={'propertyName': 'type'},
)
)
}
),
responses={'200': Response(description='ok')},
)
parser = OperationParser(operation)
params = parser.get_parameters()
assert len(params) == 1
assert params[0].original_name == 'body'
assert params[0].py_name == 'body'
schema = parser.get_json_schema()
assert 'body' in schema['properties']
assert '' not in schema['properties']
def test_process_request_body_empty_object():
"""Test _process_request_body with a schema that is of type object but with no properties."""
operation = Operation(