From d4193dbc9b25cee54129b7d853c9a8ebc62d3576 Mon Sep 17 00:00:00 2001 From: khayashi4337 Date: Thu, 22 Oct 2026 15:42:15 +0900 Subject: [PATCH 1/4] fix(python-aiohttp, python-blueplanet, python-flask): read header params from request and avoid overwriting non-body model params Connexion only binds path, query, body, files, and context arguments to controller handlers and never passes header parameters, causing required header parameters in generated controller signatures to fail with TypeError at runtime. In addition, python-blueplanet and python-flask unconditionally deserialized non-container model parameters from request.get_json(), overwriting non-body model parameters such as $ref query parameters with the request body. Exclude header parameters from controller signatures via x-non-header-params and read them from request headers inside the handler, and scope request.get_json() deserialization to body parameters while deserializing non-body model parameters from their dict value. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../AbstractPythonConnexionServerCodegen.java | 11 ++ .../python-aiohttp/controller.mustache | 11 +- .../controllers/controller.mustache | 11 +- .../python-flask/controller.mustache | 11 +- ...tractPythonConnexionServerCodegenTest.java | 105 ++++++++++++++++++ .../3_0/python/connexion-param-sources.yaml | 48 ++++++++ .../controllers/pet_controller.py | 3 +- .../controllers/pet_controller.py | 3 +- .../controllers/pet_controller.py | 3 +- .../controllers/pet_controller.py | 3 +- 10 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/python/connexion-param-sources.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java index f7b7202d543a..f11790dce7a3 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java @@ -658,6 +658,10 @@ private List> toPyImports(CodegenModel cm, Set impor @Override public void postProcessParameter(CodegenParameter parameter) { postProcessPattern(parameter.pattern, parameter.vendorExtensions); + Object openapiName = parameter.vendorExtensions.get("x-python-connexion-openapi-name"); + if (openapiName instanceof String) { + parameter.baseName = (String) openapiName; + } } @Override @@ -666,6 +670,13 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List operationList = operations.getOperation(); for (CodegenOperation operation : operationList) { + List nonHeaderParams = new ArrayList<>(); + for (CodegenParameter param : operation.allParams) { + if (!param.isHeaderParam) { + nonHeaderParams.add(param); + } + } + operation.vendorExtensions.put("x-non-header-params", nonHeaderParams); Map skipTests = new HashMap<>(); // Set flag to deactivate tests due to connexion issue. if (operation.consumes != null) { diff --git a/modules/openapi-generator/src/main/resources/python-aiohttp/controller.mustache b/modules/openapi-generator/src/main/resources/python-aiohttp/controller.mustache index 170f65ed4b19..a84d0e81aa28 100644 --- a/modules/openapi-generator/src/main/resources/python-aiohttp/controller.mustache +++ b/modules/openapi-generator/src/main/resources/python-aiohttp/controller.mustache @@ -8,7 +8,7 @@ from {{packageName}} import util {{#operation}} -async def {{operationId}}(request: web.Request, {{#allParams}}{{paramName}}{{^required}}=None{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) -> web.Response: +async def {{operationId}}(request: web.Request, {{#vendorExtensions.x-non-header-params}}{{paramName}}{{^required}}=None{{/required}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-non-header-params}}) -> web.Response: """{{summary}}{{^summary}}{{operationId}}{{/summary}} {{notes}} @@ -57,6 +57,9 @@ async def {{operationId}}(request: web.Request, {{#allParams}}{{paramName}}{{^re """ {{#allParams}} + {{#isHeaderParam}} + {{paramName}} = request.headers.get('{{baseName}}') + {{/isHeaderParam}} {{^isContainer}} {{#isDate}} {{paramName}} = util.deserialize_date({{paramName}}) @@ -67,7 +70,13 @@ async def {{operationId}}(request: web.Request, {{#allParams}}{{paramName}}{{^re {{^isPrimitiveType}} {{^isFile}} {{^isUuid}} + {{#isBodyParam}} {{paramName}} = {{baseType}}.from_dict({{paramName}}) + {{/isBodyParam}} + {{^isBodyParam}} + if isinstance({{paramName}}, dict): + {{paramName}} = {{baseType}}{{^baseType}}{{#dataType}}{{.}}{{/dataType}}{{/baseType}}.from_dict({{paramName}}) + {{/isBodyParam}} {{/isUuid}} {{/isFile}} {{/isPrimitiveType}} diff --git a/modules/openapi-generator/src/main/resources/python-blueplanet/app/{{packageName}}/controllers/controller.mustache b/modules/openapi-generator/src/main/resources/python-blueplanet/app/{{packageName}}/controllers/controller.mustache index afba1f7cfefb..ecc66aa6aa3c 100644 --- a/modules/openapi-generator/src/main/resources/python-blueplanet/app/{{packageName}}/controllers/controller.mustache +++ b/modules/openapi-generator/src/main/resources/python-blueplanet/app/{{packageName}}/controllers/controller.mustache @@ -7,7 +7,7 @@ from {{packageName}} import util {{#operation}} -def {{operationId}}({{#allParams}}{{paramName}}{{^required}}=None{{/required}}{{^-last}}, {{/-last}}{{/allParams}}): # noqa: E501 +def {{operationId}}({{#vendorExtensions.x-non-header-params}}{{paramName}}{{^required}}=None{{/required}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-non-header-params}}): # noqa: E501 """{{summary}}{{^summary}}{{operationId}}{{/summary}} {{notes}} # noqa: E501 @@ -57,6 +57,9 @@ def {{operationId}}({{#allParams}}{{paramName}}{{^required}}=None{{/required}}{{ :rtype: {{returnType}}{{^returnType}}None{{/returnType}} """ {{#allParams}} + {{#isHeaderParam}} + {{paramName}} = connexion.request.headers.get('{{baseName}}') + {{/isHeaderParam}} {{^isContainer}} {{#isDate}} {{paramName}} = util.deserialize_date({{paramName}}) @@ -67,8 +70,14 @@ def {{operationId}}({{#allParams}}{{paramName}}{{^required}}=None{{/required}}{{ {{^isPrimitiveType}} {{^isFile}} {{^isUuid}} + {{#isBodyParam}} if connexion.request.is_json: {{paramName}} = {{baseType}}.from_dict(connexion.request.get_json()) # noqa: E501 + {{/isBodyParam}} + {{^isBodyParam}} + if isinstance({{paramName}}, dict): + {{paramName}} = {{baseType}}{{^baseType}}{{#dataType}}{{.}}{{/dataType}}{{/baseType}}.from_dict({{paramName}}) # noqa: E501 + {{/isBodyParam}} {{/isUuid}} {{/isFile}} {{/isPrimitiveType}} diff --git a/modules/openapi-generator/src/main/resources/python-flask/controller.mustache b/modules/openapi-generator/src/main/resources/python-flask/controller.mustache index d5118c610a74..1aaaa46df63b 100644 --- a/modules/openapi-generator/src/main/resources/python-flask/controller.mustache +++ b/modules/openapi-generator/src/main/resources/python-flask/controller.mustache @@ -10,7 +10,7 @@ from {{packageName}} import util {{#operation}} -def {{operationId}}({{#allParams}}{{^isBodyParam}}{{paramName}}{{/isBodyParam}}{{#isBodyParam}}body{{/isBodyParam}}{{^required}}=None{{/required}}{{^-last}}, {{/-last}}{{/allParams}}): # noqa: E501 +def {{operationId}}({{#vendorExtensions.x-non-header-params}}{{^isBodyParam}}{{paramName}}{{/isBodyParam}}{{#isBodyParam}}body{{/isBodyParam}}{{^required}}=None{{/required}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-non-header-params}}): # noqa: E501 """{{summary}}{{^summary}}{{operationId}}{{/summary}} {{notes}} # noqa: E501 @@ -60,6 +60,9 @@ def {{operationId}}({{#allParams}}{{^isBodyParam}}{{paramName}}{{/isBodyParam}}{ :rtype: Union[{{returnType}}{{^returnType}}None{{/returnType}}, Tuple[{{returnType}}{{^returnType}}None{{/returnType}}, int], Tuple[{{returnType}}{{^returnType}}None{{/returnType}}, int, Dict[str, str]] """ {{#allParams}} + {{#isHeaderParam}} + {{paramName}} = flask.request.headers.get('{{baseName}}') + {{/isHeaderParam}} {{#isBodyParam}} {{paramName}} = body {{/isBodyParam}} @@ -73,8 +76,14 @@ def {{operationId}}({{#allParams}}{{^isBodyParam}}{{paramName}}{{/isBodyParam}}{ {{^isPrimitiveType}} {{^isFile}} {{^isUuid}} + {{#isBodyParam}} if flask.request.is_json: {{paramName}} = {{baseType}}{{^baseType}}{{#dataType}} {{.}}{{/dataType}}{{/baseType}}.from_dict(flask.request.get_json()) # noqa: E501 + {{/isBodyParam}} + {{^isBodyParam}} + if isinstance({{paramName}}, dict): + {{paramName}} = {{baseType}}{{^baseType}}{{#dataType}}{{.}}{{/dataType}}{{/baseType}}.from_dict({{paramName}}) # noqa: E501 + {{/isBodyParam}} {{/isUuid}} {{/isFile}} {{/isPrimitiveType}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/AbstractPythonConnexionServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/AbstractPythonConnexionServerCodegenTest.java index c4133bf1ccd5..f37b003130bf 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/AbstractPythonConnexionServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/AbstractPythonConnexionServerCodegenTest.java @@ -3,16 +3,26 @@ import com.google.common.collect.ImmutableMap; import org.apache.commons.lang3.StringUtils; import org.openapitools.codegen.CodegenConstants; +import org.openapitools.codegen.DefaultGenerator; +import org.openapitools.codegen.config.CodegenConfigurator; import org.openapitools.codegen.languages.AbstractPythonConnexionServerCodegen; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; +import static org.openapitools.codegen.TestUtils.assertFileContains; +import static org.openapitools.codegen.TestUtils.assertFileExists; +import static org.openapitools.codegen.TestUtils.assertFileNotContains; import static org.openapitools.codegen.languages.AbstractPythonConnexionServerCodegen.*; public class AbstractPythonConnexionServerCodegenTest { @@ -142,4 +152,99 @@ private static String platformAgnosticPath(String... nodes) { return StringUtils.join(nodes, File.separatorChar); } + private static Path generateController(String generatorName, Map additionalProperties, String... controllerPathSegments) throws IOException { + File output = Files.createTempDirectory("test-" + generatorName).toFile().getCanonicalFile(); + output.deleteOnExit(); + + CodegenConfigurator config = new CodegenConfigurator() + .setGeneratorName(generatorName) + .setInputSpec("src/test/resources/3_0/python/connexion-param-sources.yaml") + .setOutputDir(output.getAbsolutePath()); + for (Map.Entry entry : additionalProperties.entrySet()) { + config.addAdditionalProperty(entry.getKey(), entry.getValue()); + } + + List files = new DefaultGenerator().opts(config.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + Path controller = Paths.get(output.getAbsolutePath(), controllerPathSegments); + assertFileExists(controller); + return controller; + } + + @Test(description = "header parameters are read from request headers rather than declared as Connexion handler arguments") + public void testHeaderParamsReadFromRequestHeadersInConnexionControllers() throws IOException { + Path aiohttpController = generateController( + "python-aiohttp", + Collections.emptyMap(), + "openapi_server", "controllers", "default_controller.py"); + assertFileContains( + aiohttpController, + "async def create_item(request: web.Request, filter_param, body) -> web.Response:", + "x_req_header = request.headers.get('X-Req-Header')"); + assertFileNotContains(aiohttpController, "async def create_item(request: web.Request, x_req_header"); + + Path blueplanetController = generateController( + "python-blueplanet", + Collections.emptyMap(), + "app", "openapi_server", "controllers", "default_controller.py"); + assertFileContains( + blueplanetController, + "def create_item(filter_param, body): # noqa: E501", + "x_req_header = connexion.request.headers.get('X-Req-Header')"); + assertFileNotContains(blueplanetController, "def create_item(x_req_header"); + + for (Map props : new Map[]{Collections.emptyMap(), ImmutableMap.of("useConnexion3", "true")}) { + Path flaskController = generateController( + "python-flask", + props, + "openapi_server", "controllers", "default_controller.py"); + assertFileContains( + flaskController, + "def create_item(filter_param, body): # noqa: E501", + "x_req_header = flask.request.headers.get('X-Req-Header')"); + assertFileNotContains(flaskController, "def create_item(x_req_header"); + } + } + + @Test(description = "non-body model parameters are deserialized from their argument dict rather than overwritten from request.get_json()") + public void testNonBodyModelParamsNotOverwrittenFromRequestBody() throws IOException { + Path aiohttpController = generateController( + "python-aiohttp", + Collections.emptyMap(), + "openapi_server", "controllers", "default_controller.py"); + assertFileContains( + aiohttpController, + "if isinstance(filter_param, dict):", + "filter_param = QueryFilter.from_dict(filter_param)", + "body = BodyPayload.from_dict(body)"); + assertFileNotContains(aiohttpController, "= .from_dict("); + + Path blueplanetController = generateController( + "python-blueplanet", + Collections.emptyMap(), + "app", "openapi_server", "controllers", "default_controller.py"); + assertFileContains( + blueplanetController, + "if isinstance(filter_param, dict):", + "filter_param = QueryFilter.from_dict(filter_param) # noqa: E501", + "body = BodyPayload.from_dict(connexion.request.get_json()) # noqa: E501"); + assertFileNotContains(blueplanetController, "QueryFilter.from_dict(connexion.request.get_json())"); + assertFileNotContains(blueplanetController, "= .from_dict("); + + for (Map props : new Map[]{Collections.emptyMap(), ImmutableMap.of("useConnexion3", "true")}) { + Path flaskController = generateController( + "python-flask", + props, + "openapi_server", "controllers", "default_controller.py"); + assertFileContains( + flaskController, + "if isinstance(filter_param, dict):", + "filter_param = QueryFilter.from_dict(filter_param) # noqa: E501", + "body_payload = body", + "body_payload = BodyPayload.from_dict(flask.request.get_json()) # noqa: E501"); + assertFileNotContains(flaskController, "QueryFilter.from_dict(flask.request.get_json())"); + } + } + } diff --git a/modules/openapi-generator/src/test/resources/3_0/python/connexion-param-sources.yaml b/modules/openapi-generator/src/test/resources/3_0/python/connexion-param-sources.yaml new file mode 100644 index 000000000000..2914df12d847 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/python/connexion-param-sources.yaml @@ -0,0 +1,48 @@ +openapi: 3.0.3 +info: + title: Connexion parameter sources test + version: 1.0.0 +servers: + - url: /v1 +paths: + /items: + post: + operationId: create_item + parameters: + - name: X-Req-Header + in: header + required: true + schema: + type: string + - name: filter_param + in: query + required: true + style: deepObject + explode: true + schema: + $ref: '#/components/schemas/QueryFilter' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BodyPayload' + responses: + '200': + description: OK + content: + application/json: + schema: + type: object +components: + schemas: + QueryFilter: + type: object + properties: + tag: + type: string + BodyPayload: + type: object + properties: + item_name: + type: string diff --git a/samples/server/petstore/python-aiohttp-srclayout/src/openapi_server/controllers/pet_controller.py b/samples/server/petstore/python-aiohttp-srclayout/src/openapi_server/controllers/pet_controller.py index ad7557832bab..908f776a5401 100644 --- a/samples/server/petstore/python-aiohttp-srclayout/src/openapi_server/controllers/pet_controller.py +++ b/samples/server/petstore/python-aiohttp-srclayout/src/openapi_server/controllers/pet_controller.py @@ -19,7 +19,7 @@ async def add_pet(request: web.Request, body) -> web.Response: return web.Response(status=200) -async def delete_pet(request: web.Request, pet_id, api_key=None) -> web.Response: +async def delete_pet(request: web.Request, pet_id) -> web.Response: """Deletes a pet @@ -30,6 +30,7 @@ async def delete_pet(request: web.Request, pet_id, api_key=None) -> web.Response :type api_key: str """ + api_key = request.headers.get('api_key') return web.Response(status=200) diff --git a/samples/server/petstore/python-blueplanet/app/openapi_server/controllers/pet_controller.py b/samples/server/petstore/python-blueplanet/app/openapi_server/controllers/pet_controller.py index 9a6239dc04d5..7ef5d0d62f2f 100644 --- a/samples/server/petstore/python-blueplanet/app/openapi_server/controllers/pet_controller.py +++ b/samples/server/petstore/python-blueplanet/app/openapi_server/controllers/pet_controller.py @@ -20,7 +20,7 @@ def add_pet(body): # noqa: E501 return 'do some magic!' -def delete_pet(pet_id, api_key=None): # noqa: E501 +def delete_pet(pet_id): # noqa: E501 """Deletes a pet # noqa: E501 @@ -32,6 +32,7 @@ def delete_pet(pet_id, api_key=None): # noqa: E501 :rtype: None """ + api_key = connexion.request.headers.get('api_key') return 'do some magic!' diff --git a/samples/server/petstore/python-flask-connexion3/openapi_server/controllers/pet_controller.py b/samples/server/petstore/python-flask-connexion3/openapi_server/controllers/pet_controller.py index 9023d28028d1..62d9ba72c5e7 100644 --- a/samples/server/petstore/python-flask-connexion3/openapi_server/controllers/pet_controller.py +++ b/samples/server/petstore/python-flask-connexion3/openapi_server/controllers/pet_controller.py @@ -24,7 +24,7 @@ def add_pet(body): # noqa: E501 return 'do some magic!' -def delete_pet(pet_id, api_key=None): # noqa: E501 +def delete_pet(pet_id): # noqa: E501 """Deletes a pet # noqa: E501 @@ -36,6 +36,7 @@ def delete_pet(pet_id, api_key=None): # noqa: E501 :rtype: Union[None, Tuple[None, int], Tuple[None, int, Dict[str, str]] """ + api_key = flask.request.headers.get('api_key') return 'do some magic!' diff --git a/samples/server/petstore/python-flask/openapi_server/controllers/pet_controller.py b/samples/server/petstore/python-flask/openapi_server/controllers/pet_controller.py index 9023d28028d1..62d9ba72c5e7 100644 --- a/samples/server/petstore/python-flask/openapi_server/controllers/pet_controller.py +++ b/samples/server/petstore/python-flask/openapi_server/controllers/pet_controller.py @@ -24,7 +24,7 @@ def add_pet(body): # noqa: E501 return 'do some magic!' -def delete_pet(pet_id, api_key=None): # noqa: E501 +def delete_pet(pet_id): # noqa: E501 """Deletes a pet # noqa: E501 @@ -36,6 +36,7 @@ def delete_pet(pet_id, api_key=None): # noqa: E501 :rtype: Union[None, Tuple[None, int], Tuple[None, int, Dict[str, str]] """ + api_key = flask.request.headers.get('api_key') return 'do some magic!' From 910dcae3fdbb6b0ef51792b43b09666b583f8cf5 Mon Sep 17 00:00:00 2001 From: khayashi4337 Date: Thu, 22 Oct 2026 17:03:40 +0900 Subject: [PATCH 2/4] fix(python-aiohttp, python-blueplanet, python-flask): handle cookie params, header coercion/defaults, and non-body container params Address Codex review findings on Connexion parameter handling: - Scope request.get_json() deserialization for array and map parameters in python-flask and python-blueplanet to body parameters, deserializing non-body list/dict arguments from their passed value. - Convert integer, number,boolean, and JSON-content header/cookie parameters read from request headers/cookies and honor schema default values when omitted. - Use baseName instead of paramName for header keys in generated controller_test.mustache across python-aiohttp, python-blueplanet, and python-flask so required headers pass Connexion validation. - Exclude cookie parameters from controller signatures and read them from request.cookies.get('{{baseName}}') inside the handler. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../AbstractPythonConnexionServerCodegen.java | 18 ++- .../python-aiohttp/controller.mustache | 24 +++- .../python-aiohttp/controller_test.mustache | 2 +- .../controllers/controller.mustache | 60 ++++++++- .../test/controller_test.mustache | 2 +- .../python-flask/controller.mustache | 60 ++++++++- .../python-flask/controller_test.mustache | 2 +- ...tractPythonConnexionServerCodegenTest.java | 121 ++++++++++++------ .../3_0/python/connexion-param-sources.yaml | 51 ++++++++ 9 files changed, 297 insertions(+), 43 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java index f11790dce7a3..07b5869ff90d 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java @@ -662,6 +662,15 @@ public void postProcessParameter(CodegenParameter parameter) { if (openapiName instanceof String) { parameter.baseName = (String) openapiName; } + if (parameter.isHeaderParam || parameter.isCookieParam) { + if (parameter.isInteger || parameter.isLong) { + parameter.vendorExtensions.put("x-python-connexion-cast-int", Boolean.TRUE); + } else if (parameter.isNumber || parameter.isFloat || parameter.isDouble) { + parameter.vendorExtensions.put("x-python-connexion-cast-float", Boolean.TRUE); + } else if (parameter.isBoolean) { + parameter.vendorExtensions.put("x-python-connexion-cast-bool", Boolean.TRUE); + } + } } @Override @@ -669,12 +678,16 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List operationList = operations.getOperation(); + boolean hasJsonHeaderParam = false; for (CodegenOperation operation : operationList) { List nonHeaderParams = new ArrayList<>(); for (CodegenParameter param : operation.allParams) { - if (!param.isHeaderParam) { + if (!param.isHeaderParam && !param.isCookieParam) { nonHeaderParams.add(param); } + if (param.headerIsJsonMimeType) { + hasJsonHeaderParam = true; + } } operation.vendorExtensions.put("x-non-header-params", nonHeaderParams); Map skipTests = new HashMap<>(); @@ -736,6 +749,9 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List additionalProperties, String... controllerPathSegments) throws IOException { + private static Path generateOutputDir(String generatorName, Map additionalProperties) throws IOException { File output = Files.createTempDirectory("test-" + generatorName).toFile().getCanonicalFile(); output.deleteOnExit(); @@ -167,52 +167,93 @@ private static Path generateController(String generatorName, Map List files = new DefaultGenerator().opts(config.toClientOptInput()).generate(); files.forEach(File::deleteOnExit); - Path controller = Paths.get(output.getAbsolutePath(), controllerPathSegments); - assertFileExists(controller); - return controller; + return output.toPath(); } - @Test(description = "header parameters are read from request headers rather than declared as Connexion handler arguments") + @Test(description = "header and cookie parameters are read from request with type conversion and defaults rather than declared as Connexion handler arguments") public void testHeaderParamsReadFromRequestHeadersInConnexionControllers() throws IOException { - Path aiohttpController = generateController( - "python-aiohttp", - Collections.emptyMap(), - "openapi_server", "controllers", "default_controller.py"); + Path aiohttpDir = generateOutputDir("python-aiohttp", Collections.emptyMap()); + Path aiohttpController = aiohttpDir.resolve(Paths.get("openapi_server", "controllers", "default_controller.py")); + Path aiohttpTest = aiohttpDir.resolve(Paths.get("tests", "test_default_controller.py")); + assertFileExists(aiohttpController); + assertFileExists(aiohttpTest); assertFileContains( aiohttpController, - "async def create_item(request: web.Request, filter_param, body) -> web.Response:", - "x_req_header = request.headers.get('X-Req-Header')"); + "import json", + "async def create_item(request: web.Request, filter_param, body, date_list=None, model_map=None) -> web.Response:", + "x_req_header = request.headers.get('X-Req-Header')", + "x_int_header = request.headers.get('X-Int-Header', 7)", + "x_int_header = int(x_int_header)", + "x_num_header = request.headers.get('X-Num-Header', 1.5)", + "x_num_header = float(x_num_header)", + "x_bool_header = request.headers.get('X-Bool-Header', True)", + "x_bool_header = x_bool_header.lower() == 'true'", + "x_json_header = request.headers.get('X-Json-Header')", + "x_json_header = json.loads(x_json_header)", + "x_json_header = HeaderArg.from_dict(x_json_header)", + "session_token = request.cookies.get('session_token', 'default-cookie')"); assertFileNotContains(aiohttpController, "async def create_item(request: web.Request, x_req_header"); + assertFileNotContains(aiohttpController, "session_token=None"); + assertFileContains(aiohttpTest, "'X-Req-Header':"); + assertFileNotContains(aiohttpTest, "'x_req_header':"); - Path blueplanetController = generateController( - "python-blueplanet", - Collections.emptyMap(), - "app", "openapi_server", "controllers", "default_controller.py"); + Path blueplanetDir = generateOutputDir("python-blueplanet", Collections.emptyMap()); + Path blueplanetController = blueplanetDir.resolve(Paths.get("app", "openapi_server", "controllers", "default_controller.py")); + Path blueplanetTest = blueplanetDir.resolve(Paths.get("app", "openapi_server", "test", "test_default_controller.py")); + assertFileExists(blueplanetController); + assertFileExists(blueplanetTest); assertFileContains( blueplanetController, - "def create_item(filter_param, body): # noqa: E501", - "x_req_header = connexion.request.headers.get('X-Req-Header')"); + "import json", + "def create_item(filter_param, body, date_list=None, model_map=None): # noqa: E501", + "x_req_header = connexion.request.headers.get('X-Req-Header')", + "x_int_header = connexion.request.headers.get('X-Int-Header', 7)", + "x_int_header = int(x_int_header)", + "x_num_header = connexion.request.headers.get('X-Num-Header', 1.5)", + "x_num_header = float(x_num_header)", + "x_bool_header = connexion.request.headers.get('X-Bool-Header', True)", + "x_bool_header = x_bool_header.lower() == 'true'", + "x_json_header = connexion.request.headers.get('X-Json-Header')", + "x_json_header = json.loads(x_json_header)", + "x_json_header = HeaderArg.from_dict(x_json_header) # noqa: E501", + "session_token = connexion.request.cookies.get('session_token', 'default-cookie')"); assertFileNotContains(blueplanetController, "def create_item(x_req_header"); + assertFileNotContains(blueplanetController, "session_token=None"); + assertFileContains(blueplanetTest, "('X-Req-Header',"); + assertFileNotContains(blueplanetTest, "('x_req_header',"); for (Map props : new Map[]{Collections.emptyMap(), ImmutableMap.of("useConnexion3", "true")}) { - Path flaskController = generateController( - "python-flask", - props, - "openapi_server", "controllers", "default_controller.py"); + Path flaskDir = generateOutputDir("python-flask", props); + Path flaskController = flaskDir.resolve(Paths.get("openapi_server", "controllers", "default_controller.py")); + Path flaskTest = flaskDir.resolve(Paths.get("openapi_server", "test", "test_default_controller.py")); + assertFileExists(flaskController); + assertFileExists(flaskTest); assertFileContains( flaskController, - "def create_item(filter_param, body): # noqa: E501", - "x_req_header = flask.request.headers.get('X-Req-Header')"); + "import json", + "def create_item(filter_param, body, date_list=None, model_map=None): # noqa: E501", + "x_req_header = flask.request.headers.get('X-Req-Header')", + "x_int_header = flask.request.headers.get('X-Int-Header', 7)", + "x_int_header = int(x_int_header)", + "x_num_header = flask.request.headers.get('X-Num-Header', 1.5)", + "x_num_header = float(x_num_header)", + "x_bool_header = flask.request.headers.get('X-Bool-Header', True)", + "x_bool_header = x_bool_header.lower() == 'true'", + "x_json_header = flask.request.headers.get('X-Json-Header')", + "x_json_header = json.loads(x_json_header)", + "x_json_header = HeaderArg.from_dict(x_json_header) # noqa: E501", + "session_token = flask.request.cookies.get('session_token', 'default-cookie')"); assertFileNotContains(flaskController, "def create_item(x_req_header"); + assertFileNotContains(flaskController, "session_token=None"); + assertFileContains(flaskTest, "'X-Req-Header':"); + assertFileNotContains(flaskTest, "'x_req_header':"); } } - @Test(description = "non-body model parameters are deserialized from their argument dict rather than overwritten from request.get_json()") + @Test(description = "non-body model, array, and map parameters are deserialized from their argument value rather than overwritten from request.get_json()") public void testNonBodyModelParamsNotOverwrittenFromRequestBody() throws IOException { - Path aiohttpController = generateController( - "python-aiohttp", - Collections.emptyMap(), - "openapi_server", "controllers", "default_controller.py"); + Path aiohttpController = generateOutputDir("python-aiohttp", Collections.emptyMap()) + .resolve(Paths.get("openapi_server", "controllers", "default_controller.py")); assertFileContains( aiohttpController, "if isinstance(filter_param, dict):", @@ -220,30 +261,38 @@ public void testNonBodyModelParamsNotOverwrittenFromRequestBody() throws IOExcep "body = BodyPayload.from_dict(body)"); assertFileNotContains(aiohttpController, "= .from_dict("); - Path blueplanetController = generateController( - "python-blueplanet", - Collections.emptyMap(), - "app", "openapi_server", "controllers", "default_controller.py"); + Path blueplanetController = generateOutputDir("python-blueplanet", Collections.emptyMap()) + .resolve(Paths.get("app", "openapi_server", "controllers", "default_controller.py")); assertFileContains( blueplanetController, "if isinstance(filter_param, dict):", "filter_param = QueryFilter.from_dict(filter_param) # noqa: E501", + "if isinstance(date_list, list):", + "date_list = [util.deserialize_date(s) for s in date_list] # noqa: E501", + "if isinstance(model_map, dict):", + "model_map = {k: QueryFilter.from_dict(v) for k, v in model_map.items()} # noqa: E501", "body = BodyPayload.from_dict(connexion.request.get_json()) # noqa: E501"); assertFileNotContains(blueplanetController, "QueryFilter.from_dict(connexion.request.get_json())"); + assertFileNotContains(blueplanetController, "deserialize_date(s) for s in connexion.request.get_json()"); + assertFileNotContains(blueplanetController, "connexion.request.get_json().items()"); assertFileNotContains(blueplanetController, "= .from_dict("); for (Map props : new Map[]{Collections.emptyMap(), ImmutableMap.of("useConnexion3", "true")}) { - Path flaskController = generateController( - "python-flask", - props, - "openapi_server", "controllers", "default_controller.py"); + Path flaskController = generateOutputDir("python-flask", props) + .resolve(Paths.get("openapi_server", "controllers", "default_controller.py")); assertFileContains( flaskController, "if isinstance(filter_param, dict):", "filter_param = QueryFilter.from_dict(filter_param) # noqa: E501", + "if isinstance(date_list, list):", + "date_list = [util.deserialize_date(s) for s in date_list] # noqa: E501", + "if isinstance(model_map, dict):", + "model_map = {k: QueryFilter.from_dict(v) for k, v in model_map.items()} # noqa: E501", "body_payload = body", "body_payload = BodyPayload.from_dict(flask.request.get_json()) # noqa: E501"); assertFileNotContains(flaskController, "QueryFilter.from_dict(flask.request.get_json())"); + assertFileNotContains(flaskController, "deserialize_date(s) for s in flask.request.get_json()"); + assertFileNotContains(flaskController, "flask.request.get_json().items()"); } } diff --git a/modules/openapi-generator/src/test/resources/3_0/python/connexion-param-sources.yaml b/modules/openapi-generator/src/test/resources/3_0/python/connexion-param-sources.yaml index 2914df12d847..a10da9aaeb5f 100644 --- a/modules/openapi-generator/src/test/resources/3_0/python/connexion-param-sources.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/python/connexion-param-sources.yaml @@ -14,6 +14,37 @@ paths: required: true schema: type: string + - name: X-Int-Header + in: header + required: false + schema: + type: integer + default: 7 + - name: X-Num-Header + in: header + required: false + schema: + type: number + default: 1.5 + - name: X-Bool-Header + in: header + required: false + schema: + type: boolean + default: true + - name: X-Json-Header + in: header + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/HeaderArg' + - name: session_token + in: cookie + required: false + schema: + type: string + default: default-cookie - name: filter_param in: query required: true @@ -21,6 +52,21 @@ paths: explode: true schema: $ref: '#/components/schemas/QueryFilter' + - name: date_list + in: query + required: false + schema: + type: array + items: + type: string + format: date + - name: model_map + in: query + required: false + schema: + type: object + additionalProperties: + $ref: '#/components/schemas/QueryFilter' requestBody: required: true content: @@ -36,6 +82,11 @@ paths: type: object components: schemas: + HeaderArg: + type: object + properties: + mode: + type: string QueryFilter: type: object properties: From ec1eeeeda4da64b5ee6b382975c54453122bb178 Mon Sep 17 00:00:00 2001 From: khayashi4337 Date: Thu, 22 Oct 2026 17:24:00 +0900 Subject: [PATCH 3/4] fix(python-aiohttp, python-blueplanet, python-flask): format array defaults as Python literals and support array/JSON cookies - Format array schema defaults as valid Python list literals (quoting strings and capitalizing True/False for both Jackson ArrayNode and List defaults) so header/cookie array defaults do not emit bare identifiers or lowercase booleans. - Split non-JSON array header and cookie values on ',' and coerce integer/number/boolean array items, and decode JSON-content cookies via json.loads before model deserialization. - Guard non-body date, datetime, and model array comprehensions in python-aiohttp with isinstance(..., list). - Rename the x-non-header-params vendor extension to x-handler-params to reflect that both header and cookie parameters are excluded. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../AbstractPythonConnexionServerCodegen.java | 65 +++++++++++++++++-- .../python-aiohttp/controller.mustache | 40 +++++++++++- .../controllers/controller.mustache | 22 ++++++- .../python-flask/controller.mustache | 22 ++++++- ...tractPythonConnexionServerCodegenTest.java | 46 ++++++++++++- .../3_0/python/connexion-param-sources.yaml | 45 +++++++++++++ 6 files changed, 223 insertions(+), 17 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java index 07b5869ff90d..c4876d9d370c 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java @@ -655,6 +655,43 @@ private List> toPyImports(CodegenModel cm, Set impor return pyImports; } + @Override + public String toDefaultValue(Schema p) { + Schema unaliased = p != null ? ModelUtils.unaliasSchema(this.openAPI, p) : null; + Object rawDefault = p != null && p.getDefault() != null ? p.getDefault() + : (unaliased != null ? unaliased.getDefault() : null); + Schema target = unaliased != null ? unaliased : p; + if (target != null && ModelUtils.isArraySchema(target) && rawDefault instanceof Iterable) { + Schema items = ModelUtils.getSchemaItems(target); + if (items != null) { + items = ModelUtils.unaliasSchema(this.openAPI, items); + } + List formatted = new ArrayList<>(); + for (Object item : (Iterable) rawDefault) { + if (item == null || (item instanceof com.fasterxml.jackson.databind.JsonNode && ((com.fasterxml.jackson.databind.JsonNode) item).isNull())) { + formatted.add("None"); + } else if (item instanceof com.fasterxml.jackson.databind.JsonNode) { + com.fasterxml.jackson.databind.JsonNode node = (com.fasterxml.jackson.databind.JsonNode) item; + if (node.isBoolean() || (items != null && ModelUtils.isBooleanSchema(items))) { + formatted.add(node.asBoolean() ? "True" : "False"); + } else if (node.isNumber() || (items != null && (ModelUtils.isIntegerSchema(items) || ModelUtils.isNumberSchema(items)))) { + formatted.add(node.asText()); + } else { + formatted.add(formatPythonStringLiteral(node.asText())); + } + } else if (item instanceof Boolean || (items != null && ModelUtils.isBooleanSchema(items))) { + formatted.add(Boolean.parseBoolean(item.toString()) ? "True" : "False"); + } else if (item instanceof Number || (items != null && (ModelUtils.isIntegerSchema(items) || ModelUtils.isNumberSchema(items)))) { + formatted.add(item.toString()); + } else { + formatted.add(formatPythonStringLiteral(item.toString())); + } + } + return "[" + String.join(", ", formatted) + "]"; + } + return super.toDefaultValue(p); + } + @Override public void postProcessParameter(CodegenParameter parameter) { postProcessPattern(parameter.pattern, parameter.vendorExtensions); @@ -663,7 +700,25 @@ public void postProcessParameter(CodegenParameter parameter) { parameter.baseName = (String) openapiName; } if (parameter.isHeaderParam || parameter.isCookieParam) { - if (parameter.isInteger || parameter.isLong) { + boolean isJsonContent = parameter.headerIsJsonMimeType + || isJsonMimeType(parameter.contentType) + || isJsonVendorMimeType(parameter.contentType); + if (isJsonContent) { + parameter.vendorExtensions.put("x-python-connexion-json-content", Boolean.TRUE); + } else if (parameter.isArray) { + if (parameter.items == null || parameter.items.complexType == null) { + parameter.vendorExtensions.put("x-python-connexion-split-csv", Boolean.TRUE); + } + if (parameter.items != null) { + if (parameter.items.isInteger || parameter.items.isLong) { + parameter.vendorExtensions.put("x-python-connexion-cast-int-items", Boolean.TRUE); + } else if (parameter.items.isNumber || parameter.items.isFloat || parameter.items.isDouble) { + parameter.vendorExtensions.put("x-python-connexion-cast-float-items", Boolean.TRUE); + } else if (parameter.items.isBoolean) { + parameter.vendorExtensions.put("x-python-connexion-cast-bool-items", Boolean.TRUE); + } + } + } else if (parameter.isInteger || parameter.isLong) { parameter.vendorExtensions.put("x-python-connexion-cast-int", Boolean.TRUE); } else if (parameter.isNumber || parameter.isFloat || parameter.isDouble) { parameter.vendorExtensions.put("x-python-connexion-cast-float", Boolean.TRUE); @@ -680,16 +735,16 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List nonHeaderParams = new ArrayList<>(); + List handlerParams = new ArrayList<>(); for (CodegenParameter param : operation.allParams) { if (!param.isHeaderParam && !param.isCookieParam) { - nonHeaderParams.add(param); + handlerParams.add(param); } - if (param.headerIsJsonMimeType) { + if (Boolean.TRUE.equals(param.vendorExtensions.get("x-python-connexion-json-content"))) { hasJsonHeaderParam = true; } } - operation.vendorExtensions.put("x-non-header-params", nonHeaderParams); + operation.vendorExtensions.put("x-handler-params", handlerParams); Map skipTests = new HashMap<>(); // Set flag to deactivate tests due to connexion issue. if (operation.consumes != null) { diff --git a/modules/openapi-generator/src/main/resources/python-aiohttp/controller.mustache b/modules/openapi-generator/src/main/resources/python-aiohttp/controller.mustache index 8afa8cc3b766..acbe257a85d5 100644 --- a/modules/openapi-generator/src/main/resources/python-aiohttp/controller.mustache +++ b/modules/openapi-generator/src/main/resources/python-aiohttp/controller.mustache @@ -11,7 +11,7 @@ from {{packageName}} import util {{#operation}} -async def {{operationId}}(request: web.Request, {{#vendorExtensions.x-non-header-params}}{{paramName}}{{^required}}=None{{/required}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-non-header-params}}) -> web.Response: +async def {{operationId}}(request: web.Request, {{#vendorExtensions.x-handler-params}}{{paramName}}{{^required}}=None{{/required}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-handler-params}}) -> web.Response: """{{summary}}{{^summary}}{{operationId}}{{/summary}} {{notes}} @@ -78,10 +78,26 @@ async def {{operationId}}(request: web.Request, {{#vendorExtensions.x-non-header if isinstance({{paramName}}, str): {{paramName}} = {{paramName}}.lower() == 'true' {{/vendorExtensions.x-python-connexion-cast-bool}} - {{#headerIsJsonMimeType}} + {{#vendorExtensions.x-python-connexion-json-content}} if isinstance({{paramName}}, str): {{paramName}} = json.loads({{paramName}}) - {{/headerIsJsonMimeType}} + {{/vendorExtensions.x-python-connexion-json-content}} + {{#vendorExtensions.x-python-connexion-split-csv}} + if isinstance({{paramName}}, str): + {{paramName}} = {{paramName}}.split(',') + {{/vendorExtensions.x-python-connexion-split-csv}} + {{#vendorExtensions.x-python-connexion-cast-int-items}} + if isinstance({{paramName}}, list): + {{paramName}} = [int(x) for x in {{paramName}}] + {{/vendorExtensions.x-python-connexion-cast-int-items}} + {{#vendorExtensions.x-python-connexion-cast-float-items}} + if isinstance({{paramName}}, list): + {{paramName}} = [float(x) for x in {{paramName}}] + {{/vendorExtensions.x-python-connexion-cast-float-items}} + {{#vendorExtensions.x-python-connexion-cast-bool-items}} + if isinstance({{paramName}}, list): + {{paramName}} = [x.lower() == 'true' if isinstance(x, str) else bool(x) for x in {{paramName}}] + {{/vendorExtensions.x-python-connexion-cast-bool-items}} {{^isContainer}} {{#isDate}} {{paramName}} = util.deserialize_date({{paramName}}) @@ -106,13 +122,31 @@ async def {{operationId}}(request: web.Request, {{#vendorExtensions.x-non-header {{#isArray}} {{#items}} {{#isDate}} + {{#isBodyParam}} {{paramName}} = [util.deserialize_date(s) for s in {{paramName}}] + {{/isBodyParam}} + {{^isBodyParam}} + if isinstance({{paramName}}, list): + {{paramName}} = [util.deserialize_date(s) for s in {{paramName}}] + {{/isBodyParam}} {{/isDate}} {{#isDateTime}} + {{#isBodyParam}} {{paramName}} = [util.deserialize_datetime(s) for s in {{paramName}}] + {{/isBodyParam}} + {{^isBodyParam}} + if isinstance({{paramName}}, list): + {{paramName}} = [util.deserialize_datetime(s) for s in {{paramName}}] + {{/isBodyParam}} {{/isDateTime}} {{#complexType}} + {{#isBodyParam}} {{paramName}} = [{{complexType}}.from_dict(d) for d in {{paramName}}] + {{/isBodyParam}} + {{^isBodyParam}} + if isinstance({{paramName}}, list): + {{paramName}} = [{{complexType}}.from_dict(d) for d in {{paramName}}] + {{/isBodyParam}} {{/complexType}} {{/items}} {{/isArray}} diff --git a/modules/openapi-generator/src/main/resources/python-blueplanet/app/{{packageName}}/controllers/controller.mustache b/modules/openapi-generator/src/main/resources/python-blueplanet/app/{{packageName}}/controllers/controller.mustache index 095f5a2dec7a..d20d1f3363f6 100644 --- a/modules/openapi-generator/src/main/resources/python-blueplanet/app/{{packageName}}/controllers/controller.mustache +++ b/modules/openapi-generator/src/main/resources/python-blueplanet/app/{{packageName}}/controllers/controller.mustache @@ -10,7 +10,7 @@ from {{packageName}} import util {{#operation}} -def {{operationId}}({{#vendorExtensions.x-non-header-params}}{{paramName}}{{^required}}=None{{/required}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-non-header-params}}): # noqa: E501 +def {{operationId}}({{#vendorExtensions.x-handler-params}}{{paramName}}{{^required}}=None{{/required}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-handler-params}}): # noqa: E501 """{{summary}}{{^summary}}{{operationId}}{{/summary}} {{notes}} # noqa: E501 @@ -78,10 +78,26 @@ def {{operationId}}({{#vendorExtensions.x-non-header-params}}{{paramName}}{{^req if isinstance({{paramName}}, str): {{paramName}} = {{paramName}}.lower() == 'true' {{/vendorExtensions.x-python-connexion-cast-bool}} - {{#headerIsJsonMimeType}} + {{#vendorExtensions.x-python-connexion-json-content}} if isinstance({{paramName}}, str): {{paramName}} = json.loads({{paramName}}) - {{/headerIsJsonMimeType}} + {{/vendorExtensions.x-python-connexion-json-content}} + {{#vendorExtensions.x-python-connexion-split-csv}} + if isinstance({{paramName}}, str): + {{paramName}} = {{paramName}}.split(',') + {{/vendorExtensions.x-python-connexion-split-csv}} + {{#vendorExtensions.x-python-connexion-cast-int-items}} + if isinstance({{paramName}}, list): + {{paramName}} = [int(x) for x in {{paramName}}] + {{/vendorExtensions.x-python-connexion-cast-int-items}} + {{#vendorExtensions.x-python-connexion-cast-float-items}} + if isinstance({{paramName}}, list): + {{paramName}} = [float(x) for x in {{paramName}}] + {{/vendorExtensions.x-python-connexion-cast-float-items}} + {{#vendorExtensions.x-python-connexion-cast-bool-items}} + if isinstance({{paramName}}, list): + {{paramName}} = [x.lower() == 'true' if isinstance(x, str) else bool(x) for x in {{paramName}}] + {{/vendorExtensions.x-python-connexion-cast-bool-items}} {{^isContainer}} {{#isDate}} {{paramName}} = util.deserialize_date({{paramName}}) diff --git a/modules/openapi-generator/src/main/resources/python-flask/controller.mustache b/modules/openapi-generator/src/main/resources/python-flask/controller.mustache index bd6c3b3d1b73..e0329f0ba700 100644 --- a/modules/openapi-generator/src/main/resources/python-flask/controller.mustache +++ b/modules/openapi-generator/src/main/resources/python-flask/controller.mustache @@ -13,7 +13,7 @@ from {{packageName}} import util {{#operation}} -def {{operationId}}({{#vendorExtensions.x-non-header-params}}{{^isBodyParam}}{{paramName}}{{/isBodyParam}}{{#isBodyParam}}body{{/isBodyParam}}{{^required}}=None{{/required}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-non-header-params}}): # noqa: E501 +def {{operationId}}({{#vendorExtensions.x-handler-params}}{{^isBodyParam}}{{paramName}}{{/isBodyParam}}{{#isBodyParam}}body{{/isBodyParam}}{{^required}}=None{{/required}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-handler-params}}): # noqa: E501 """{{summary}}{{^summary}}{{operationId}}{{/summary}} {{notes}} # noqa: E501 @@ -81,10 +81,26 @@ def {{operationId}}({{#vendorExtensions.x-non-header-params}}{{^isBodyParam}}{{p if isinstance({{paramName}}, str): {{paramName}} = {{paramName}}.lower() == 'true' {{/vendorExtensions.x-python-connexion-cast-bool}} - {{#headerIsJsonMimeType}} + {{#vendorExtensions.x-python-connexion-json-content}} if isinstance({{paramName}}, str): {{paramName}} = json.loads({{paramName}}) - {{/headerIsJsonMimeType}} + {{/vendorExtensions.x-python-connexion-json-content}} + {{#vendorExtensions.x-python-connexion-split-csv}} + if isinstance({{paramName}}, str): + {{paramName}} = {{paramName}}.split(',') + {{/vendorExtensions.x-python-connexion-split-csv}} + {{#vendorExtensions.x-python-connexion-cast-int-items}} + if isinstance({{paramName}}, list): + {{paramName}} = [int(x) for x in {{paramName}}] + {{/vendorExtensions.x-python-connexion-cast-int-items}} + {{#vendorExtensions.x-python-connexion-cast-float-items}} + if isinstance({{paramName}}, list): + {{paramName}} = [float(x) for x in {{paramName}}] + {{/vendorExtensions.x-python-connexion-cast-float-items}} + {{#vendorExtensions.x-python-connexion-cast-bool-items}} + if isinstance({{paramName}}, list): + {{paramName}} = [x.lower() == 'true' if isinstance(x, str) else bool(x) for x in {{paramName}}] + {{/vendorExtensions.x-python-connexion-cast-bool-items}} {{#isBodyParam}} {{paramName}} = body {{/isBodyParam}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/AbstractPythonConnexionServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/AbstractPythonConnexionServerCodegenTest.java index e24cf4b422cb..cde8d813d027 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/AbstractPythonConnexionServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/AbstractPythonConnexionServerCodegenTest.java @@ -191,9 +191,25 @@ public void testHeaderParamsReadFromRequestHeadersInConnexionControllers() throw "x_json_header = request.headers.get('X-Json-Header')", "x_json_header = json.loads(x_json_header)", "x_json_header = HeaderArg.from_dict(x_json_header)", - "session_token = request.cookies.get('session_token', 'default-cookie')"); + "x_tag_list = request.headers.get('X-Tag-List', ['foo', 'bar'])", + "x_tag_list = x_tag_list.split(',')", + "x_flag_list = request.headers.get('X-Flag-List', [True, False])", + "x_flag_list = [x.lower() == 'true' if isinstance(x, str) else bool(x) for x in x_flag_list]", + "x_date_list = request.headers.get('X-Date-List')", + "x_date_list = x_date_list.split(',')", + "if isinstance(x_date_list, list):", + "x_date_list = [util.deserialize_date(s) for s in x_date_list]", + "session_token = request.cookies.get('session_token', 'default-cookie')", + "ids = request.cookies.get('ids', [1, 2])", + "ids = ids.split(',')", + "ids = [int(x) for x in ids]", + "json_cookie = request.cookies.get('json_cookie')", + "json_cookie = json.loads(json_cookie)", + "json_cookie = HeaderArg.from_dict(json_cookie)"); assertFileNotContains(aiohttpController, "async def create_item(request: web.Request, x_req_header"); assertFileNotContains(aiohttpController, "session_token=None"); + assertFileNotContains(aiohttpController, "[foo, bar]"); + assertFileNotContains(aiohttpController, "[true, false]"); assertFileContains(aiohttpTest, "'X-Req-Header':"); assertFileNotContains(aiohttpTest, "'x_req_header':"); @@ -216,9 +232,21 @@ public void testHeaderParamsReadFromRequestHeadersInConnexionControllers() throw "x_json_header = connexion.request.headers.get('X-Json-Header')", "x_json_header = json.loads(x_json_header)", "x_json_header = HeaderArg.from_dict(x_json_header) # noqa: E501", - "session_token = connexion.request.cookies.get('session_token', 'default-cookie')"); + "x_tag_list = connexion.request.headers.get('X-Tag-List', ['foo', 'bar'])", + "x_tag_list = x_tag_list.split(',')", + "x_flag_list = connexion.request.headers.get('X-Flag-List', [True, False])", + "x_flag_list = [x.lower() == 'true' if isinstance(x, str) else bool(x) for x in x_flag_list]", + "session_token = connexion.request.cookies.get('session_token', 'default-cookie')", + "ids = connexion.request.cookies.get('ids', [1, 2])", + "ids = ids.split(',')", + "ids = [int(x) for x in ids]", + "json_cookie = connexion.request.cookies.get('json_cookie')", + "json_cookie = json.loads(json_cookie)", + "json_cookie = HeaderArg.from_dict(json_cookie) # noqa: E501"); assertFileNotContains(blueplanetController, "def create_item(x_req_header"); assertFileNotContains(blueplanetController, "session_token=None"); + assertFileNotContains(blueplanetController, "[foo, bar]"); + assertFileNotContains(blueplanetController, "[true, false]"); assertFileContains(blueplanetTest, "('X-Req-Header',"); assertFileNotContains(blueplanetTest, "('x_req_header',"); @@ -242,9 +270,21 @@ public void testHeaderParamsReadFromRequestHeadersInConnexionControllers() throw "x_json_header = flask.request.headers.get('X-Json-Header')", "x_json_header = json.loads(x_json_header)", "x_json_header = HeaderArg.from_dict(x_json_header) # noqa: E501", - "session_token = flask.request.cookies.get('session_token', 'default-cookie')"); + "x_tag_list = flask.request.headers.get('X-Tag-List', ['foo', 'bar'])", + "x_tag_list = x_tag_list.split(',')", + "x_flag_list = flask.request.headers.get('X-Flag-List', [True, False])", + "x_flag_list = [x.lower() == 'true' if isinstance(x, str) else bool(x) for x in x_flag_list]", + "session_token = flask.request.cookies.get('session_token', 'default-cookie')", + "ids = flask.request.cookies.get('ids', [1, 2])", + "ids = ids.split(',')", + "ids = [int(x) for x in ids]", + "json_cookie = flask.request.cookies.get('json_cookie')", + "json_cookie = json.loads(json_cookie)", + "json_cookie = HeaderArg.from_dict(json_cookie) # noqa: E501"); assertFileNotContains(flaskController, "def create_item(x_req_header"); assertFileNotContains(flaskController, "session_token=None"); + assertFileNotContains(flaskController, "[foo, bar]"); + assertFileNotContains(flaskController, "[true, false]"); assertFileContains(flaskTest, "'X-Req-Header':"); assertFileNotContains(flaskTest, "'x_req_header':"); } diff --git a/modules/openapi-generator/src/test/resources/3_0/python/connexion-param-sources.yaml b/modules/openapi-generator/src/test/resources/3_0/python/connexion-param-sources.yaml index a10da9aaeb5f..73e18fa04c4c 100644 --- a/modules/openapi-generator/src/test/resources/3_0/python/connexion-param-sources.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/python/connexion-param-sources.yaml @@ -39,12 +39,57 @@ paths: application/json: schema: $ref: '#/components/schemas/HeaderArg' + - name: X-Tag-List + in: header + required: false + schema: + type: array + items: + type: string + default: + - foo + - bar + - name: X-Flag-List + in: header + required: false + schema: + type: array + items: + type: boolean + default: + - true + - false + - name: X-Date-List + in: header + required: false + schema: + type: array + items: + type: string + format: date - name: session_token in: cookie required: false schema: type: string default: default-cookie + - name: ids + in: cookie + required: false + schema: + type: array + items: + type: integer + default: + - 1 + - 2 + - name: json_cookie + in: cookie + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/HeaderArg' - name: filter_param in: query required: true From cf71763fed115aaa124e64102502712d118f834f Mon Sep 17 00:00:00 2001 From: khayashi4337 Date: Fri, 23 Oct 2026 12:16:04 +0900 Subject: [PATCH 4/4] fix(python-aiohttp, python-blueplanet, python-flask): guard None date params, format nested array defaults, and split enum-ref array headers Address cubic-dev-ai review findings on PR #25362: - Guard non-container date and date-time deserialization with `if is not None:` across python-aiohttp, python-blueplanet, and python-flask so omitted optional date/date-time headers or cookies do not pass None into dateutil.parser.parse(). - Recursively format object and nested array elements in array schema defaults as Python dict and list literals instead of calling JsonNode.asText() (which returns '' for container nodes). - Split non-JSON array header/cookie values when items are non-object referenced schemas (such as $ref enum models where !items.isModel) while keeping object-model arrays unsplit unless JSON content is used. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../AbstractPythonConnexionServerCodegen.java | 95 ++++++++++++++----- .../python-aiohttp/controller.mustache | 6 +- .../controllers/controller.mustache | 6 +- .../python-flask/controller.mustache | 6 +- ...tractPythonConnexionServerCodegenTest.java | 42 ++++++++ .../3_0/python/connexion-param-sources.yaml | 47 +++++++++ 6 files changed, 172 insertions(+), 30 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java index c4876d9d370c..fde192bcac67 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java @@ -662,34 +662,81 @@ public String toDefaultValue(Schema p) { : (unaliased != null ? unaliased.getDefault() : null); Schema target = unaliased != null ? unaliased : p; if (target != null && ModelUtils.isArraySchema(target) && rawDefault instanceof Iterable) { - Schema items = ModelUtils.getSchemaItems(target); - if (items != null) { - items = ModelUtils.unaliasSchema(this.openAPI, items); + return formatPythonDefaultValue(rawDefault, target); + } + return super.toDefaultValue(p); + } + + private String formatPythonDefaultValue(Object value, Schema schema) { + Schema target = schema != null ? ModelUtils.unaliasSchema(this.openAPI, schema) : null; + if (value == null || (value instanceof com.fasterxml.jackson.databind.JsonNode + && ((com.fasterxml.jackson.databind.JsonNode) value).isNull())) { + return "None"; + } + if (value instanceof com.fasterxml.jackson.databind.JsonNode) { + com.fasterxml.jackson.databind.JsonNode node = (com.fasterxml.jackson.databind.JsonNode) value; + if (node.isArray()) { + Schema itemSchema = target != null ? ModelUtils.getSchemaItems(target) : null; + List elements = new ArrayList<>(); + for (com.fasterxml.jackson.databind.JsonNode elem : node) { + elements.add(formatPythonDefaultValue(elem, itemSchema)); + } + return "[" + String.join(", ", elements) + "]"; } - List formatted = new ArrayList<>(); - for (Object item : (Iterable) rawDefault) { - if (item == null || (item instanceof com.fasterxml.jackson.databind.JsonNode && ((com.fasterxml.jackson.databind.JsonNode) item).isNull())) { - formatted.add("None"); - } else if (item instanceof com.fasterxml.jackson.databind.JsonNode) { - com.fasterxml.jackson.databind.JsonNode node = (com.fasterxml.jackson.databind.JsonNode) item; - if (node.isBoolean() || (items != null && ModelUtils.isBooleanSchema(items))) { - formatted.add(node.asBoolean() ? "True" : "False"); - } else if (node.isNumber() || (items != null && (ModelUtils.isIntegerSchema(items) || ModelUtils.isNumberSchema(items)))) { - formatted.add(node.asText()); - } else { - formatted.add(formatPythonStringLiteral(node.asText())); + if (node.isObject()) { + List entries = new ArrayList<>(); + Iterator> fields = node.fields(); + while (fields.hasNext()) { + Map.Entry entry = fields.next(); + Schema propSchema = null; + if (target != null && target.getProperties() != null) { + propSchema = (Schema) target.getProperties().get(entry.getKey()); + } else if (target != null && ModelUtils.getAdditionalProperties(target) != null) { + propSchema = ModelUtils.getAdditionalProperties(target); } - } else if (item instanceof Boolean || (items != null && ModelUtils.isBooleanSchema(items))) { - formatted.add(Boolean.parseBoolean(item.toString()) ? "True" : "False"); - } else if (item instanceof Number || (items != null && (ModelUtils.isIntegerSchema(items) || ModelUtils.isNumberSchema(items)))) { - formatted.add(item.toString()); - } else { - formatted.add(formatPythonStringLiteral(item.toString())); + entries.add(formatPythonStringLiteral(entry.getKey()) + ": " + + formatPythonDefaultValue(entry.getValue(), propSchema)); } + return "{" + String.join(", ", entries) + "}"; + } + if (node.isBoolean() || (target != null && ModelUtils.isBooleanSchema(target))) { + return node.asBoolean() ? "True" : "False"; + } + if (node.isNumber() || (target != null && (ModelUtils.isIntegerSchema(target) || ModelUtils.isNumberSchema(target)))) { + return node.asText(); } - return "[" + String.join(", ", formatted) + "]"; + return formatPythonStringLiteral(node.asText()); } - return super.toDefaultValue(p); + if (value instanceof Iterable) { + Schema itemSchema = target != null ? ModelUtils.getSchemaItems(target) : null; + List elements = new ArrayList<>(); + for (Object elem : (Iterable) value) { + elements.add(formatPythonDefaultValue(elem, itemSchema)); + } + return "[" + String.join(", ", elements) + "]"; + } + if (value instanceof Map) { + List entries = new ArrayList<>(); + for (Map.Entry entry : ((Map) value).entrySet()) { + String key = String.valueOf(entry.getKey()); + Schema propSchema = null; + if (target != null && target.getProperties() != null) { + propSchema = (Schema) target.getProperties().get(key); + } else if (target != null && ModelUtils.getAdditionalProperties(target) != null) { + propSchema = ModelUtils.getAdditionalProperties(target); + } + entries.add(formatPythonStringLiteral(key) + ": " + + formatPythonDefaultValue(entry.getValue(), propSchema)); + } + return "{" + String.join(", ", entries) + "}"; + } + if (value instanceof Boolean || (target != null && ModelUtils.isBooleanSchema(target))) { + return Boolean.parseBoolean(value.toString()) ? "True" : "False"; + } + if (value instanceof Number || (target != null && (ModelUtils.isIntegerSchema(target) || ModelUtils.isNumberSchema(target)))) { + return value.toString(); + } + return formatPythonStringLiteral(value.toString()); } @Override @@ -706,7 +753,7 @@ public void postProcessParameter(CodegenParameter parameter) { if (isJsonContent) { parameter.vendorExtensions.put("x-python-connexion-json-content", Boolean.TRUE); } else if (parameter.isArray) { - if (parameter.items == null || parameter.items.complexType == null) { + if (parameter.items == null || !parameter.items.isModel) { parameter.vendorExtensions.put("x-python-connexion-split-csv", Boolean.TRUE); } if (parameter.items != null) { diff --git a/modules/openapi-generator/src/main/resources/python-aiohttp/controller.mustache b/modules/openapi-generator/src/main/resources/python-aiohttp/controller.mustache index acbe257a85d5..f0b157f75917 100644 --- a/modules/openapi-generator/src/main/resources/python-aiohttp/controller.mustache +++ b/modules/openapi-generator/src/main/resources/python-aiohttp/controller.mustache @@ -100,10 +100,12 @@ async def {{operationId}}(request: web.Request, {{#vendorExtensions.x-handler-pa {{/vendorExtensions.x-python-connexion-cast-bool-items}} {{^isContainer}} {{#isDate}} - {{paramName}} = util.deserialize_date({{paramName}}) + if {{paramName}} is not None: + {{paramName}} = util.deserialize_date({{paramName}}) {{/isDate}} {{#isDateTime}} - {{paramName}} = util.deserialize_datetime({{paramName}}) + if {{paramName}} is not None: + {{paramName}} = util.deserialize_datetime({{paramName}}) {{/isDateTime}} {{^isPrimitiveType}} {{^isFile}} diff --git a/modules/openapi-generator/src/main/resources/python-blueplanet/app/{{packageName}}/controllers/controller.mustache b/modules/openapi-generator/src/main/resources/python-blueplanet/app/{{packageName}}/controllers/controller.mustache index d20d1f3363f6..8c116a15c70a 100644 --- a/modules/openapi-generator/src/main/resources/python-blueplanet/app/{{packageName}}/controllers/controller.mustache +++ b/modules/openapi-generator/src/main/resources/python-blueplanet/app/{{packageName}}/controllers/controller.mustache @@ -100,10 +100,12 @@ def {{operationId}}({{#vendorExtensions.x-handler-params}}{{paramName}}{{^requir {{/vendorExtensions.x-python-connexion-cast-bool-items}} {{^isContainer}} {{#isDate}} - {{paramName}} = util.deserialize_date({{paramName}}) + if {{paramName}} is not None: + {{paramName}} = util.deserialize_date({{paramName}}) {{/isDate}} {{#isDateTime}} - {{paramName}} = util.deserialize_datetime({{paramName}}) + if {{paramName}} is not None: + {{paramName}} = util.deserialize_datetime({{paramName}}) {{/isDateTime}} {{^isPrimitiveType}} {{^isFile}} diff --git a/modules/openapi-generator/src/main/resources/python-flask/controller.mustache b/modules/openapi-generator/src/main/resources/python-flask/controller.mustache index e0329f0ba700..552f6de7a3af 100644 --- a/modules/openapi-generator/src/main/resources/python-flask/controller.mustache +++ b/modules/openapi-generator/src/main/resources/python-flask/controller.mustache @@ -106,10 +106,12 @@ def {{operationId}}({{#vendorExtensions.x-handler-params}}{{^isBodyParam}}{{para {{/isBodyParam}} {{^isContainer}} {{#isDate}} - {{paramName}} = util.deserialize_date({{paramName}}) + if {{paramName}} is not None: + {{paramName}} = util.deserialize_date({{paramName}}) {{/isDate}} {{#isDateTime}} - {{paramName}} = util.deserialize_datetime({{paramName}}) + if {{paramName}} is not None: + {{paramName}} = util.deserialize_datetime({{paramName}}) {{/isDateTime}} {{^isPrimitiveType}} {{^isFile}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/AbstractPythonConnexionServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/AbstractPythonConnexionServerCodegenTest.java index cde8d813d027..a0b1cc5edf94 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/AbstractPythonConnexionServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/AbstractPythonConnexionServerCodegenTest.java @@ -188,6 +188,9 @@ public void testHeaderParamsReadFromRequestHeadersInConnexionControllers() throw "x_num_header = float(x_num_header)", "x_bool_header = request.headers.get('X-Bool-Header', True)", "x_bool_header = x_bool_header.lower() == 'true'", + "x_opt_date = request.headers.get('X-Opt-Date')", + "if x_opt_date is not None:", + "x_opt_date = util.deserialize_date(x_opt_date)", "x_json_header = request.headers.get('X-Json-Header')", "x_json_header = json.loads(x_json_header)", "x_json_header = HeaderArg.from_dict(x_json_header)", @@ -199,7 +202,16 @@ public void testHeaderParamsReadFromRequestHeadersInConnexionControllers() throw "x_date_list = x_date_list.split(',')", "if isinstance(x_date_list, list):", "x_date_list = [util.deserialize_date(s) for s in x_date_list]", + "x_status_list = request.headers.get('X-Status-List')", + "x_status_list = x_status_list.split(',')", + "x_status_list = [StatusEnum.from_dict(d) for d in x_status_list]", + "x_obj_list = request.headers.get('X-Obj-List', [{'tag': 'alpha'}, {'tag': 'beta'}])", + "x_obj_list = [QueryFilter.from_dict(d) for d in x_obj_list]", + "x_nested_list = request.headers.get('X-Nested-List', [['a', 'b'], ['c']])", "session_token = request.cookies.get('session_token', 'default-cookie')", + "opt_datetime = request.cookies.get('opt_datetime')", + "if opt_datetime is not None:", + "opt_datetime = util.deserialize_datetime(opt_datetime)", "ids = request.cookies.get('ids', [1, 2])", "ids = ids.split(',')", "ids = [int(x) for x in ids]", @@ -210,6 +222,8 @@ public void testHeaderParamsReadFromRequestHeadersInConnexionControllers() throw assertFileNotContains(aiohttpController, "session_token=None"); assertFileNotContains(aiohttpController, "[foo, bar]"); assertFileNotContains(aiohttpController, "[true, false]"); + assertFileNotContains(aiohttpController, "['', '']"); + assertFileNotContains(aiohttpController, "x_obj_list.split(',')"); assertFileContains(aiohttpTest, "'X-Req-Header':"); assertFileNotContains(aiohttpTest, "'x_req_header':"); @@ -229,6 +243,9 @@ public void testHeaderParamsReadFromRequestHeadersInConnexionControllers() throw "x_num_header = float(x_num_header)", "x_bool_header = connexion.request.headers.get('X-Bool-Header', True)", "x_bool_header = x_bool_header.lower() == 'true'", + "x_opt_date = connexion.request.headers.get('X-Opt-Date')", + "if x_opt_date is not None:", + "x_opt_date = util.deserialize_date(x_opt_date)", "x_json_header = connexion.request.headers.get('X-Json-Header')", "x_json_header = json.loads(x_json_header)", "x_json_header = HeaderArg.from_dict(x_json_header) # noqa: E501", @@ -236,7 +253,16 @@ public void testHeaderParamsReadFromRequestHeadersInConnexionControllers() throw "x_tag_list = x_tag_list.split(',')", "x_flag_list = connexion.request.headers.get('X-Flag-List', [True, False])", "x_flag_list = [x.lower() == 'true' if isinstance(x, str) else bool(x) for x in x_flag_list]", + "x_status_list = connexion.request.headers.get('X-Status-List')", + "x_status_list = x_status_list.split(',')", + "x_status_list = [StatusEnum.from_dict(d) for d in x_status_list] # noqa: E501", + "x_obj_list = connexion.request.headers.get('X-Obj-List', [{'tag': 'alpha'}, {'tag': 'beta'}])", + "x_obj_list = [QueryFilter.from_dict(d) for d in x_obj_list] # noqa: E501", + "x_nested_list = connexion.request.headers.get('X-Nested-List', [['a', 'b'], ['c']])", "session_token = connexion.request.cookies.get('session_token', 'default-cookie')", + "opt_datetime = connexion.request.cookies.get('opt_datetime')", + "if opt_datetime is not None:", + "opt_datetime = util.deserialize_datetime(opt_datetime)", "ids = connexion.request.cookies.get('ids', [1, 2])", "ids = ids.split(',')", "ids = [int(x) for x in ids]", @@ -247,6 +273,8 @@ public void testHeaderParamsReadFromRequestHeadersInConnexionControllers() throw assertFileNotContains(blueplanetController, "session_token=None"); assertFileNotContains(blueplanetController, "[foo, bar]"); assertFileNotContains(blueplanetController, "[true, false]"); + assertFileNotContains(blueplanetController, "['', '']"); + assertFileNotContains(blueplanetController, "x_obj_list.split(',')"); assertFileContains(blueplanetTest, "('X-Req-Header',"); assertFileNotContains(blueplanetTest, "('x_req_header',"); @@ -267,6 +295,9 @@ public void testHeaderParamsReadFromRequestHeadersInConnexionControllers() throw "x_num_header = float(x_num_header)", "x_bool_header = flask.request.headers.get('X-Bool-Header', True)", "x_bool_header = x_bool_header.lower() == 'true'", + "x_opt_date = flask.request.headers.get('X-Opt-Date')", + "if x_opt_date is not None:", + "x_opt_date = util.deserialize_date(x_opt_date)", "x_json_header = flask.request.headers.get('X-Json-Header')", "x_json_header = json.loads(x_json_header)", "x_json_header = HeaderArg.from_dict(x_json_header) # noqa: E501", @@ -274,7 +305,16 @@ public void testHeaderParamsReadFromRequestHeadersInConnexionControllers() throw "x_tag_list = x_tag_list.split(',')", "x_flag_list = flask.request.headers.get('X-Flag-List', [True, False])", "x_flag_list = [x.lower() == 'true' if isinstance(x, str) else bool(x) for x in x_flag_list]", + "x_status_list = flask.request.headers.get('X-Status-List')", + "x_status_list = x_status_list.split(',')", + "x_status_list = [StatusEnum.from_dict(d) for d in x_status_list] # noqa: E501", + "x_obj_list = flask.request.headers.get('X-Obj-List', [{'tag': 'alpha'}, {'tag': 'beta'}])", + "x_obj_list = [QueryFilter.from_dict(d) for d in x_obj_list] # noqa: E501", + "x_nested_list = flask.request.headers.get('X-Nested-List', [['a', 'b'], ['c']])", "session_token = flask.request.cookies.get('session_token', 'default-cookie')", + "opt_datetime = flask.request.cookies.get('opt_datetime')", + "if opt_datetime is not None:", + "opt_datetime = util.deserialize_datetime(opt_datetime)", "ids = flask.request.cookies.get('ids', [1, 2])", "ids = ids.split(',')", "ids = [int(x) for x in ids]", @@ -285,6 +325,8 @@ public void testHeaderParamsReadFromRequestHeadersInConnexionControllers() throw assertFileNotContains(flaskController, "session_token=None"); assertFileNotContains(flaskController, "[foo, bar]"); assertFileNotContains(flaskController, "[true, false]"); + assertFileNotContains(flaskController, "['', '']"); + assertFileNotContains(flaskController, "x_obj_list.split(',')"); assertFileContains(flaskTest, "'X-Req-Header':"); assertFileNotContains(flaskTest, "'x_req_header':"); } diff --git a/modules/openapi-generator/src/test/resources/3_0/python/connexion-param-sources.yaml b/modules/openapi-generator/src/test/resources/3_0/python/connexion-param-sources.yaml index 73e18fa04c4c..5b2fed5337cc 100644 --- a/modules/openapi-generator/src/test/resources/3_0/python/connexion-param-sources.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/python/connexion-param-sources.yaml @@ -32,6 +32,12 @@ paths: schema: type: boolean default: true + - name: X-Opt-Date + in: header + required: false + schema: + type: string + format: date - name: X-Json-Header in: header required: false @@ -67,12 +73,48 @@ paths: items: type: string format: date + - name: X-Status-List + in: header + required: false + schema: + type: array + items: + $ref: '#/components/schemas/StatusEnum' + - name: X-Obj-List + in: header + required: false + schema: + type: array + items: + $ref: '#/components/schemas/QueryFilter' + default: + - tag: alpha + - tag: beta + - name: X-Nested-List + in: header + required: false + schema: + type: array + items: + type: array + items: + type: string + default: + - - a + - b + - - c - name: session_token in: cookie required: false schema: type: string default: default-cookie + - name: opt_datetime + in: cookie + required: false + schema: + type: string + format: date-time - name: ids in: cookie required: false @@ -127,6 +169,11 @@ paths: type: object components: schemas: + StatusEnum: + type: string + enum: + - active + - archived HeaderArg: type: object properties: