From 4e148092a4b1ecd08fd2f66e630250f2f6ab719f Mon Sep 17 00:00:00 2001 From: Jakub Novak Date: Thu, 7 Aug 2025 13:03:28 +0200 Subject: [PATCH 1/3] Introduce handling for the `explode` property for arrays in OpenAPI parameter parsing --- .../__snapshots__/test_end_to_end.ambr | 2 +- end_to_end_tests/baseline_openapi_3.0.json | 13 +++++ end_to_end_tests/baseline_openapi_3.1.yaml | 13 +++++ .../test_docstrings.py | 8 ++++ .../api/tests/get_user_list.py | 17 +++++++ openapi_python_client/parser/openapi.py | 1 + .../parser/properties/__init__.py | 2 + .../parser/properties/list_property.py | 4 ++ .../schema/openapi_schema_pydantic/header.py | 2 + .../openapi_schema_pydantic/parameter.py | 47 +++++++++++++++++-- openapi_python_client/schema/style.py | 18 +++++++ .../templates/endpoint_macros.py.jinja | 4 ++ .../test_properties/test_schemas.py | 3 +- 13 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 openapi_python_client/schema/style.py diff --git a/end_to_end_tests/__snapshots__/test_end_to_end.ambr b/end_to_end_tests/__snapshots__/test_end_to_end.ambr index 525f8baf2..ca9194a5f 100644 --- a/end_to_end_tests/__snapshots__/test_end_to_end.ambr +++ b/end_to_end_tests/__snapshots__/test_end_to_end.ambr @@ -69,7 +69,7 @@ Path parameter must be required - Parameter(name='optional', param_in=, description=None, required=False, deprecated=False, allowEmptyValue=False, style=None, explode=False, allowReserved=False, param_schema=Schema(title=None, multipleOf=None, maximum=None, exclusiveMaximum=None, minimum=None, exclusiveMinimum=None, maxLength=None, minLength=None, pattern=None, maxItems=None, minItems=None, uniqueItems=None, maxProperties=None, minProperties=None, required=None, enum=None, const=None, type=, allOf=[], oneOf=[], anyOf=[], schema_not=None, items=None, prefixItems=[], properties=None, additionalProperties=None, description=None, schema_format=None, default=None, nullable=False, discriminator=None, readOnly=None, writeOnly=None, xml=None, externalDocs=None, example=None, deprecated=None), example=None, examples=None, content=None) + Parameter(name='optional', param_in=, description=None, required=False, deprecated=False, allowEmptyValue=False, style=, explode=False, allowReserved=False, param_schema=Schema(title=None, multipleOf=None, maximum=None, exclusiveMaximum=None, minimum=None, exclusiveMinimum=None, maxLength=None, minLength=None, pattern=None, maxItems=None, minItems=None, uniqueItems=None, maxProperties=None, minProperties=None, required=None, enum=None, const=None, type=, allOf=[], oneOf=[], anyOf=[], schema_not=None, items=None, prefixItems=[], properties=None, additionalProperties=None, description=None, schema_format=None, default=None, nullable=False, discriminator=None, readOnly=None, writeOnly=None, xml=None, externalDocs=None, example=None, deprecated=None), example=None, examples=None, content=None) If you believe this was a mistake or this tool is missing a feature you need, please open an issue at https://github.com/openapi-generators/openapi-python-client/issues/new/choose diff --git a/end_to_end_tests/baseline_openapi_3.0.json b/end_to_end_tests/baseline_openapi_3.0.json index b536f7432..653b3357f 100644 --- a/end_to_end_tests/baseline_openapi_3.0.json +++ b/end_to_end_tests/baseline_openapi_3.0.json @@ -149,6 +149,19 @@ "name": "an_enum_value_with_only_null", "in": "query" }, + { + "required": true, + "schema": { + "title": "Non exploded array", + "type": "array", + "items": { + "type": "string" + } + }, + "name": "non_exploded_array", + "in": "query", + "explode": false + }, { "required": true, "schema": { diff --git a/end_to_end_tests/baseline_openapi_3.1.yaml b/end_to_end_tests/baseline_openapi_3.1.yaml index b9fb3e9f6..5c61326c8 100644 --- a/end_to_end_tests/baseline_openapi_3.1.yaml +++ b/end_to_end_tests/baseline_openapi_3.1.yaml @@ -145,6 +145,19 @@ info: "name": "an_enum_value_with_only_null", "in": "query" }, + { + "required": true, + "schema": { + "title": "Non exploded array", + "type": "array", + "items": { + "type": "string" + } + }, + "name": "non_exploded_array", + "in": "query", + "explode": false + }, { "required": true, "schema": { diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_docstrings.py b/end_to_end_tests/functional_tests/generated_code_execution/test_docstrings.py index d2d560780..59df12b5f 100644 --- a/end_to_end_tests/functional_tests/generated_code_execution/test_docstrings.py +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_docstrings.py @@ -111,6 +111,13 @@ def test_model_properties(self, MyModel): schema: type: boolean description: Do you want fries with that? + - name: array + in: query + required: false + schema: + type: array + items: + type: string responses: "200": description: Success! @@ -160,4 +167,5 @@ def test_params(self, get_attribute_by_index_sync): "id (str): Which one.", "index (int):", "fries (Union[Unset, bool]): Do you want fries with that?", + "array (Union[Unset, list[str]]):", ] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py index a708cf71d..c73b44e9b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py @@ -18,6 +18,7 @@ def _get_kwargs( an_enum_value: list[AnEnum], an_enum_value_with_null: list[Union[AnEnumWithNull, None]], an_enum_value_with_only_null: list[None], + non_exploded_array: list[str], some_date: Union[datetime.date, datetime.datetime], ) -> dict[str, Any]: params: dict[str, Any] = {} @@ -44,6 +45,10 @@ def _get_kwargs( params["an_enum_value_with_only_null"] = json_an_enum_value_with_only_null + json_non_exploded_array = non_exploded_array + + params["non_exploded_array"] = ",".join(str(item) for item in json_non_exploded_array) + json_some_date: str if isinstance(some_date, datetime.date): json_some_date = some_date.isoformat() @@ -106,6 +111,7 @@ def sync_detailed( an_enum_value: list[AnEnum], an_enum_value_with_null: list[Union[AnEnumWithNull, None]], an_enum_value_with_only_null: list[None], + non_exploded_array: list[str], some_date: Union[datetime.date, datetime.datetime], ) -> Response[Union[HTTPValidationError, list["AModel"]]]: """Get List @@ -116,6 +122,7 @@ def sync_detailed( an_enum_value (list[AnEnum]): an_enum_value_with_null (list[Union[AnEnumWithNull, None]]): an_enum_value_with_only_null (list[None]): + non_exploded_array (list[str]): some_date (Union[datetime.date, datetime.datetime]): Raises: @@ -130,6 +137,7 @@ def sync_detailed( an_enum_value=an_enum_value, an_enum_value_with_null=an_enum_value_with_null, an_enum_value_with_only_null=an_enum_value_with_only_null, + non_exploded_array=non_exploded_array, some_date=some_date, ) @@ -146,6 +154,7 @@ def sync( an_enum_value: list[AnEnum], an_enum_value_with_null: list[Union[AnEnumWithNull, None]], an_enum_value_with_only_null: list[None], + non_exploded_array: list[str], some_date: Union[datetime.date, datetime.datetime], ) -> Optional[Union[HTTPValidationError, list["AModel"]]]: """Get List @@ -156,6 +165,7 @@ def sync( an_enum_value (list[AnEnum]): an_enum_value_with_null (list[Union[AnEnumWithNull, None]]): an_enum_value_with_only_null (list[None]): + non_exploded_array (list[str]): some_date (Union[datetime.date, datetime.datetime]): Raises: @@ -171,6 +181,7 @@ def sync( an_enum_value=an_enum_value, an_enum_value_with_null=an_enum_value_with_null, an_enum_value_with_only_null=an_enum_value_with_only_null, + non_exploded_array=non_exploded_array, some_date=some_date, ).parsed @@ -181,6 +192,7 @@ async def asyncio_detailed( an_enum_value: list[AnEnum], an_enum_value_with_null: list[Union[AnEnumWithNull, None]], an_enum_value_with_only_null: list[None], + non_exploded_array: list[str], some_date: Union[datetime.date, datetime.datetime], ) -> Response[Union[HTTPValidationError, list["AModel"]]]: """Get List @@ -191,6 +203,7 @@ async def asyncio_detailed( an_enum_value (list[AnEnum]): an_enum_value_with_null (list[Union[AnEnumWithNull, None]]): an_enum_value_with_only_null (list[None]): + non_exploded_array (list[str]): some_date (Union[datetime.date, datetime.datetime]): Raises: @@ -205,6 +218,7 @@ async def asyncio_detailed( an_enum_value=an_enum_value, an_enum_value_with_null=an_enum_value_with_null, an_enum_value_with_only_null=an_enum_value_with_only_null, + non_exploded_array=non_exploded_array, some_date=some_date, ) @@ -219,6 +233,7 @@ async def asyncio( an_enum_value: list[AnEnum], an_enum_value_with_null: list[Union[AnEnumWithNull, None]], an_enum_value_with_only_null: list[None], + non_exploded_array: list[str], some_date: Union[datetime.date, datetime.datetime], ) -> Optional[Union[HTTPValidationError, list["AModel"]]]: """Get List @@ -229,6 +244,7 @@ async def asyncio( an_enum_value (list[AnEnum]): an_enum_value_with_null (list[Union[AnEnumWithNull, None]]): an_enum_value_with_only_null (list[None]): + non_exploded_array (list[str]): some_date (Union[datetime.date, datetime.datetime]): Raises: @@ -245,6 +261,7 @@ async def asyncio( an_enum_value=an_enum_value, an_enum_value_with_null=an_enum_value_with_null, an_enum_value_with_only_null=an_enum_value_with_only_null, + non_exploded_array=non_exploded_array, some_date=some_date, ) ).parsed diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index 0aab5a717..c0833aa87 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -289,6 +289,7 @@ def add_parameters( schemas=schemas, parent_name=endpoint.name, config=config, + explode=param.explode, ) if isinstance(prop, ParseError): diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index ba667347b..df88e9b1d 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -146,6 +146,7 @@ def property_from_data( # noqa: PLR0911, PLR0912 config: Config, process_properties: bool = True, roots: set[ReferencePath | utils.ClassName] | None = None, + explode: bool | None = None, ) -> tuple[Property | PropertyError, Schemas]: """Generate a Property from the OpenAPI dictionary representation of it""" roots = roots or set() @@ -285,6 +286,7 @@ def property_from_data( # noqa: PLR0911, PLR0912 config=config, process_properties=process_properties, roots=roots, + explode=explode, ) if data.type == oai.DataType.OBJECT or data.allOf or (data.type is None and data.properties): return ModelProperty.build( diff --git a/openapi_python_client/parser/properties/list_property.py b/openapi_python_client/parser/properties/list_property.py index 06d773672..60cdf6859 100644 --- a/openapi_python_client/parser/properties/list_property.py +++ b/openapi_python_client/parser/properties/list_property.py @@ -22,6 +22,7 @@ class ListProperty(PropertyProtocol): description: str | None example: str | None inner_property: PropertyProtocol + explode: bool | None = None template: ClassVar[str] = "list_property.py.jinja" @classmethod @@ -36,6 +37,7 @@ def build( config: Config, process_properties: bool, roots: set[ReferencePath | utils.ClassName], + explode: bool | None = None, ) -> tuple[ListProperty | PropertyError, Schemas]: """ Build a ListProperty the right way, use this instead of the normal constructor. @@ -51,6 +53,7 @@ def build( property data roots: The set of `ReferencePath`s and `ClassName`s to remove from the schemas if a child reference becomes invalid + explode: Whether to use `explode` for array properties. Returns: `(result, schemas)` where `schemas` is an updated version of the input named the same including any inner @@ -98,6 +101,7 @@ def build( python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix), description=data.description, example=data.example, + explode=explode, ), schemas, ) diff --git a/openapi_python_client/schema/openapi_schema_pydantic/header.py b/openapi_python_client/schema/openapi_schema_pydantic/header.py index 2deb6f390..1813d4440 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/header.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/header.py @@ -1,6 +1,7 @@ from pydantic import ConfigDict, Field from ..parameter_location import ParameterLocation +from ..style import Style from .parameter import Parameter @@ -20,6 +21,7 @@ class Header(Parameter): name: str = Field(default="") param_in: ParameterLocation = Field(default=ParameterLocation.HEADER, alias="in") + style: Style = Field(default=Style.SIMPLE) model_config = ConfigDict( # `Parameter` is not build yet, will rebuild in `__init__.py`: defer_build=True, diff --git a/openapi_python_client/schema/openapi_schema_pydantic/parameter.py b/openapi_python_client/schema/openapi_schema_pydantic/parameter.py index bf4f4cf02..691f431b5 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/parameter.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/parameter.py @@ -1,8 +1,9 @@ from typing import Any, Optional -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, model_validator from ..parameter_location import ParameterLocation +from ..style import Style from .example import Example from .media_type import MediaType from .reference import ReferenceOr @@ -27,13 +28,53 @@ class Parameter(BaseModel): required: bool = False deprecated: bool = False allowEmptyValue: bool = False - style: Optional[str] = None - explode: bool = False + style: Optional[Style] = None + explode: Optional[bool] = None allowReserved: bool = False param_schema: Optional[ReferenceOr[Schema]] = Field(default=None, alias="schema") example: Optional[Any] = None examples: Optional[dict[str, ReferenceOr[Example]]] = None content: Optional[dict[str, MediaType]] = None + + @model_validator(mode='after') + @classmethod + def validate_dependencies(cls, model: "Parameter") -> "Parameter": + param_in = model.param_in + explode = model.explode + + if model.style is None: + if param_in in [ParameterLocation.PATH, ParameterLocation.HEADER]: + model.style = Style.SIMPLE + elif param_in in [ParameterLocation.QUERY, ParameterLocation.COOKIE]: + model.style = Style.FORM + + + # Validate style based on parameter location, not all combinations are valid. + # https://swagger.io/docs/specification/v3_0/serialization/ + if param_in == ParameterLocation.PATH: + if model.style not in (Style.SIMPLE, Style.LABEL, Style.MATRIX): + raise ValueError(f"Invalid style '{model.style}' for path parameter") + elif param_in == ParameterLocation.QUERY: + if model.style not in (Style.FORM, Style.SPACE_DELIMITED, Style.PIPE_DELIMITED, Style.DEEP_OBJECT): + raise ValueError(f"Invalid style '{model.style}' for query parameter") + elif param_in == ParameterLocation.HEADER: + if model.style != Style.SIMPLE: + raise ValueError(f"Invalid style '{model.style}' for header parameter") + elif param_in == ParameterLocation.COOKIE: + if model.style != Style.FORM: + raise ValueError(f"Invalid style '{model.style}' for cookie parameter") + + + if explode is None: + if model.style == Style.FORM: + model.explode = True + else: + model.explode = False + + return model + + + model_config = ConfigDict( # `MediaType` is not build yet, will rebuild in `__init__.py`: defer_build=True, diff --git a/openapi_python_client/schema/style.py b/openapi_python_client/schema/style.py new file mode 100644 index 000000000..cb538cfc4 --- /dev/null +++ b/openapi_python_client/schema/style.py @@ -0,0 +1,18 @@ +from enum import Enum + + +class Style(str, Enum): + """The style of a schema is defined by the style keyword + + References: + - https://swagger.io/docs/specification/v3_0/serialization/ + - https://spec.openapis.org/oas/latest.html#fixed-fields-for-use-with-schema + """ + + SIMPLE = "simple" + LABEL = "label" + MATRIX = "matrix" + FORM = "form" + SPACE_DELIMITED = "spaceDelimited" + PIPE_DELIMITED = "pipeDelimited" + DEEP_OBJECT = "deepObject" diff --git a/openapi_python_client/templates/endpoint_macros.py.jinja b/openapi_python_client/templates/endpoint_macros.py.jinja index 1b53becdd..797a82811 100644 --- a/openapi_python_client/templates/endpoint_macros.py.jinja +++ b/openapi_python_client/templates/endpoint_macros.py.jinja @@ -47,7 +47,11 @@ params: dict[str, Any] = {} {{ prop_template.transform(property, property.python_name, destination) }} {% endif %} {%- if not property.json_is_dict %} + {% if property.explode == False %} +params["{{ property.name }}"] = ",".join(str(item) for item in {{ destination }}) + {% else %} params["{{ property.name }}"] = {{ destination }} + {% endif %} {% else %} {{ guarded_statement(property, destination, "params.update(" + destination + ")") }} {% endif %} diff --git a/tests/test_parser/test_properties/test_schemas.py b/tests/test_parser/test_properties/test_schemas.py index 7e7af8514..241493430 100644 --- a/tests/test_parser/test_properties/test_schemas.py +++ b/tests/test_parser/test_properties/test_schemas.py @@ -10,6 +10,7 @@ update_parameters_with_data, ) from openapi_python_client.schema import Parameter, ParameterLocation, Reference, Schema +from openapi_python_client.schema.style import Style from openapi_python_client.utils import ClassName MODULE_NAME = "openapi_python_client.parser.properties.schemas" @@ -63,7 +64,7 @@ def test_parameters_without_schema_are_ignored(self, config): def test_registers_new_parameters(self, config): param = Parameter.model_construct( - name="a_param", param_in=ParameterLocation.QUERY, param_schema=Schema.model_construct() + name="a_param", param_in=ParameterLocation.QUERY, style=Style.FORM, explode=True, param_schema=Schema.model_construct() ) parameters = Parameters() param_or_error, new_parameters = parameter_from_data( From 10ca9c70378968e10810a410fa48a2951c2740de Mon Sep 17 00:00:00 2001 From: Jakub Novak Date: Sun, 10 Aug 2025 17:53:40 +0200 Subject: [PATCH 2/3] Handle optional correctly --- end_to_end_tests/baseline_openapi_3.0.json | 13 +++++++++++ end_to_end_tests/baseline_openapi_3.1.yaml | 14 ++++++++++++ .../api/tests/get_user_list.py | 22 ++++++++++++++++++- .../templates/endpoint_macros.py.jinja | 7 +++++- 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/end_to_end_tests/baseline_openapi_3.0.json b/end_to_end_tests/baseline_openapi_3.0.json index 653b3357f..f022370f9 100644 --- a/end_to_end_tests/baseline_openapi_3.0.json +++ b/end_to_end_tests/baseline_openapi_3.0.json @@ -162,6 +162,19 @@ "in": "query", "explode": false }, + { + "required": false, + "schema": { + "title": "Optional non exploded array", + "type": "array", + "items": { + "type": "string" + } + }, + "name": "optional_non_exploded_array", + "in": "query", + "explode": false + }, { "required": true, "schema": { diff --git a/end_to_end_tests/baseline_openapi_3.1.yaml b/end_to_end_tests/baseline_openapi_3.1.yaml index 5c61326c8..abd70bf82 100644 --- a/end_to_end_tests/baseline_openapi_3.1.yaml +++ b/end_to_end_tests/baseline_openapi_3.1.yaml @@ -158,6 +158,20 @@ info: "in": "query", "explode": false }, + + { + "required": false, + "schema": { + "title": "Optional non exploded array", + "type": "array", + "items": { + "type": "string" + } + }, + "name": "optional_non_exploded_array", + "in": "query", + "explode": false + }, { "required": true, "schema": { diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py index c73b44e9b..91187b58f 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py @@ -10,7 +10,7 @@ from ...models.an_enum import AnEnum from ...models.an_enum_with_null import AnEnumWithNull from ...models.http_validation_error import HTTPValidationError -from ...types import UNSET, Response +from ...types import UNSET, Response, Unset def _get_kwargs( @@ -19,6 +19,7 @@ def _get_kwargs( an_enum_value_with_null: list[Union[AnEnumWithNull, None]], an_enum_value_with_only_null: list[None], non_exploded_array: list[str], + optional_non_exploded_array: Union[Unset, list[str]] = UNSET, some_date: Union[datetime.date, datetime.datetime], ) -> dict[str, Any]: params: dict[str, Any] = {} @@ -49,6 +50,13 @@ def _get_kwargs( params["non_exploded_array"] = ",".join(str(item) for item in json_non_exploded_array) + json_optional_non_exploded_array: Union[Unset, list[str]] = UNSET + if not isinstance(optional_non_exploded_array, Unset): + json_optional_non_exploded_array = optional_non_exploded_array + + if not isinstance(json_optional_non_exploded_array, Unset): + params["optional_non_exploded_array"] = ",".join(str(item) for item in json_optional_non_exploded_array) + json_some_date: str if isinstance(some_date, datetime.date): json_some_date = some_date.isoformat() @@ -112,6 +120,7 @@ def sync_detailed( an_enum_value_with_null: list[Union[AnEnumWithNull, None]], an_enum_value_with_only_null: list[None], non_exploded_array: list[str], + optional_non_exploded_array: Union[Unset, list[str]] = UNSET, some_date: Union[datetime.date, datetime.datetime], ) -> Response[Union[HTTPValidationError, list["AModel"]]]: """Get List @@ -123,6 +132,7 @@ def sync_detailed( an_enum_value_with_null (list[Union[AnEnumWithNull, None]]): an_enum_value_with_only_null (list[None]): non_exploded_array (list[str]): + optional_non_exploded_array (Union[Unset, list[str]]): some_date (Union[datetime.date, datetime.datetime]): Raises: @@ -138,6 +148,7 @@ def sync_detailed( an_enum_value_with_null=an_enum_value_with_null, an_enum_value_with_only_null=an_enum_value_with_only_null, non_exploded_array=non_exploded_array, + optional_non_exploded_array=optional_non_exploded_array, some_date=some_date, ) @@ -155,6 +166,7 @@ def sync( an_enum_value_with_null: list[Union[AnEnumWithNull, None]], an_enum_value_with_only_null: list[None], non_exploded_array: list[str], + optional_non_exploded_array: Union[Unset, list[str]] = UNSET, some_date: Union[datetime.date, datetime.datetime], ) -> Optional[Union[HTTPValidationError, list["AModel"]]]: """Get List @@ -166,6 +178,7 @@ def sync( an_enum_value_with_null (list[Union[AnEnumWithNull, None]]): an_enum_value_with_only_null (list[None]): non_exploded_array (list[str]): + optional_non_exploded_array (Union[Unset, list[str]]): some_date (Union[datetime.date, datetime.datetime]): Raises: @@ -182,6 +195,7 @@ def sync( an_enum_value_with_null=an_enum_value_with_null, an_enum_value_with_only_null=an_enum_value_with_only_null, non_exploded_array=non_exploded_array, + optional_non_exploded_array=optional_non_exploded_array, some_date=some_date, ).parsed @@ -193,6 +207,7 @@ async def asyncio_detailed( an_enum_value_with_null: list[Union[AnEnumWithNull, None]], an_enum_value_with_only_null: list[None], non_exploded_array: list[str], + optional_non_exploded_array: Union[Unset, list[str]] = UNSET, some_date: Union[datetime.date, datetime.datetime], ) -> Response[Union[HTTPValidationError, list["AModel"]]]: """Get List @@ -204,6 +219,7 @@ async def asyncio_detailed( an_enum_value_with_null (list[Union[AnEnumWithNull, None]]): an_enum_value_with_only_null (list[None]): non_exploded_array (list[str]): + optional_non_exploded_array (Union[Unset, list[str]]): some_date (Union[datetime.date, datetime.datetime]): Raises: @@ -219,6 +235,7 @@ async def asyncio_detailed( an_enum_value_with_null=an_enum_value_with_null, an_enum_value_with_only_null=an_enum_value_with_only_null, non_exploded_array=non_exploded_array, + optional_non_exploded_array=optional_non_exploded_array, some_date=some_date, ) @@ -234,6 +251,7 @@ async def asyncio( an_enum_value_with_null: list[Union[AnEnumWithNull, None]], an_enum_value_with_only_null: list[None], non_exploded_array: list[str], + optional_non_exploded_array: Union[Unset, list[str]] = UNSET, some_date: Union[datetime.date, datetime.datetime], ) -> Optional[Union[HTTPValidationError, list["AModel"]]]: """Get List @@ -245,6 +263,7 @@ async def asyncio( an_enum_value_with_null (list[Union[AnEnumWithNull, None]]): an_enum_value_with_only_null (list[None]): non_exploded_array (list[str]): + optional_non_exploded_array (Union[Unset, list[str]]): some_date (Union[datetime.date, datetime.datetime]): Raises: @@ -262,6 +281,7 @@ async def asyncio( an_enum_value_with_null=an_enum_value_with_null, an_enum_value_with_only_null=an_enum_value_with_only_null, non_exploded_array=non_exploded_array, + optional_non_exploded_array=optional_non_exploded_array, some_date=some_date, ) ).parsed diff --git a/openapi_python_client/templates/endpoint_macros.py.jinja b/openapi_python_client/templates/endpoint_macros.py.jinja index 797a82811..2a35eb51a 100644 --- a/openapi_python_client/templates/endpoint_macros.py.jinja +++ b/openapi_python_client/templates/endpoint_macros.py.jinja @@ -46,9 +46,14 @@ params: dict[str, Any] = {} {% set destination = "json_" + property.python_name %} {{ prop_template.transform(property, property.python_name, destination) }} {% endif %} - {%- if not property.json_is_dict %} + {%- if not property.json_is_dict %} {% if property.explode == False %} + {% if property.required %} params["{{ property.name }}"] = ",".join(str(item) for item in {{ destination }}) + {% else %} +if not isinstance({{ destination }}, Unset): + params["{{ property.name }}"] = ",".join(str(item) for item in {{ destination }}) + {% endif %} {% else %} params["{{ property.name }}"] = {{ destination }} {% endif %} From 8d63e12c0393e251483ec73f31faa109b413869c Mon Sep 17 00:00:00 2001 From: Jakub Novak Date: Tue, 19 Aug 2025 09:31:32 +0200 Subject: [PATCH 3/3] Lint --- .../schema/openapi_schema_pydantic/parameter.py | 6 +----- tests/test_parser/test_properties/test_schemas.py | 6 +++++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openapi_python_client/schema/openapi_schema_pydantic/parameter.py b/openapi_python_client/schema/openapi_schema_pydantic/parameter.py index 691f431b5..040abff79 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/parameter.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/parameter.py @@ -36,7 +36,7 @@ class Parameter(BaseModel): examples: Optional[dict[str, ReferenceOr[Example]]] = None content: Optional[dict[str, MediaType]] = None - @model_validator(mode='after') + @model_validator(mode="after") @classmethod def validate_dependencies(cls, model: "Parameter") -> "Parameter": param_in = model.param_in @@ -48,7 +48,6 @@ def validate_dependencies(cls, model: "Parameter") -> "Parameter": elif param_in in [ParameterLocation.QUERY, ParameterLocation.COOKIE]: model.style = Style.FORM - # Validate style based on parameter location, not all combinations are valid. # https://swagger.io/docs/specification/v3_0/serialization/ if param_in == ParameterLocation.PATH: @@ -64,7 +63,6 @@ def validate_dependencies(cls, model: "Parameter") -> "Parameter": if model.style != Style.FORM: raise ValueError(f"Invalid style '{model.style}' for cookie parameter") - if explode is None: if model.style == Style.FORM: model.explode = True @@ -73,8 +71,6 @@ def validate_dependencies(cls, model: "Parameter") -> "Parameter": return model - - model_config = ConfigDict( # `MediaType` is not build yet, will rebuild in `__init__.py`: defer_build=True, diff --git a/tests/test_parser/test_properties/test_schemas.py b/tests/test_parser/test_properties/test_schemas.py index 241493430..e10ead21f 100644 --- a/tests/test_parser/test_properties/test_schemas.py +++ b/tests/test_parser/test_properties/test_schemas.py @@ -64,7 +64,11 @@ def test_parameters_without_schema_are_ignored(self, config): def test_registers_new_parameters(self, config): param = Parameter.model_construct( - name="a_param", param_in=ParameterLocation.QUERY, style=Style.FORM, explode=True, param_schema=Schema.model_construct() + name="a_param", + param_in=ParameterLocation.QUERY, + style=Style.FORM, + explode=True, + param_schema=Schema.model_construct(), ) parameters = Parameters() param_or_error, new_parameters = parameter_from_data(