Skip to content

fix(python-flask, python-aiohttp): accept OpenAPI 3 form request bodies the way Connexion passes them - #25456

Open
khayashi4337 wants to merge 5 commits into
OpenAPITools:masterfrom
khayashi4337:fix/python-connexion-form-body
Open

khayashi4337 wants to merge 5 commits into
OpenAPITools:masterfrom
khayashi4337:fix/python-connexion-form-body

Conversation

@khayashi4337

@khayashi4337 khayashi4337 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Connexion passes an OpenAPI 3 form request body (application/x-www-form-urlencoded or multipart/form-data) to the handler as one argument holding the non-file fields, already type-coerced, named by the requestBody's x-body-name (default body). Binary file fields are passed as separate arguments. I checked this on Connexion 2.14.2 (flask and aiohttp) and 3.3.0 (flask).

The generated python-flask and python-aiohttp handlers declare one argument per form field instead:

  • an operation with a required form field fails on every request with TypeError: ... missing 1 required positional argument (500);
  • optional form fields are silently dropped.

The petstore samples are affected, e.g. def update_pet_with_form(pet_id, name=None, status=None) and upload_file(...).

Changes

  • For operations that consume form content (the media type is compared case-insensitively), the non-file form fields are collapsed into that single argument. File fields stay separate arguments.
  • The controller templates copy the argument to a collision-safe local and unpack each field by its wire name into the same local variable names as before. So the rest of the template code (date conversion etc.) is unchanged, and a form field named body doesn't overwrite the dict:
    def update_pet_with_form(pet_id, body=None):
        _form_body = body or {}
        name = _form_body.get('name')
        status = _form_body.get('status')
  • The argument name is the requestBody's x-body-name. For Connexion 2 only, the schema-level x-body-name that it still reads comes next; Connexion 3 ignores it. Otherwise the name is body. The docstring shows it as dict.
  • python-blueplanet (Connexion 1.x) is unchanged.
  • Regenerated the petstore samples for python-flask, python-flask (Connexion 3) and python-aiohttp (pet_controller.py only).
  • The dict argument can still collide with another handler argument of the same Python name, because Connexion passes the body dict, the path and query parameters, and the file fields all as keyword arguments. Connexion 2.14.2 and 3.3.0 both behave as follows:
    • Path or query parameter with the same name: the dict overwrites it, so the parameter never reaches the handler. It is dropped from the generated signature, and a warning suggests a distinct x-body-name.
    • File field with the same name: the file wins over the dict, so the file stays an argument, and a warning is logged.
    • Header or cookie parameter with the same name: it is bound to a local variable of the same name, so a warning is logged and the form dict is read defensively (isinstance(..., dict)).
  • Form field names are emitted as escaped Python string literals (_form_body.get('it\'s')), so names with quotes, backslashes or control characters produce valid code and read the right key.
  • Webhook operations resolve their requestBody from webhooks too, so x-body-name and required are honored there.
  • The docstrings of the collapsed handlers no longer list the form fields as :params. Each field's name, type and description is folded into the dict argument's :param line, e.g. :param body: Form fields: name (str): Updated name of the pet; status (str): Updated status of the pet.

Verification

  • Python*Test / AbstractPython*Test pass. The new tests cover flask, flask with useConnexion3, and aiohttp: required and optional bodies, requestBody and schema-level x-body-name, file + fields, a camelCase path parameter with a required body, a field named body, an upper-case media type. They also check that blueplanet keeps its per-field arguments. The tests fail without the change.

  • Generated servers and sent real requests, on Connexion 2.14.2 (flask), Connexion 3.3.0 (flask, useConnexion3) and Connexion 2.14.2 (aiohttp):

    • urlencoded name=a&size=3&body=hello: 200, with name='a', size=3 (int) and body='hello' delivered;
    • multipart name=a plus a file: 200, with name and the file delivered.

    With the generator from master, the urlencoded request returns 500, and the multipart one returns 200 but loses name.

  • bin/generate-samples.sh bin/configs/python*.yaml: the only changes are the three petstore pet_controller.py files.

Known limitations (Connexion's side, unchanged)

  • Connexion 2 doesn't pass form bodies of GET/DELETE operations.
  • A file field is only passed when its property name is already a valid snake_case Python name: Connexion compares the raw multipart key with the function's argument names.
  • An x-body-name that is a Python builtin (e.g. input) or callback only matches Connexion's naming together with fix(python-flask, python-aiohttp): name handler arguments the way Connexion passes them #25450.
  • The generated controller tests for form operations are still skipped, as before.

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    Regenerated all bin/configs/python*.yaml samples and committed the three changed controllers. Generator options and docs are unchanged.
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

cc @cbornet @tomplus @krjakbrjak @fa0311 (Python technical committee)

Parts of this change were drafted with AI coding assistants (Devin, Claude Code, with reviews by Codex and Fable) and reviewed and verified by me.

🤖 Generated with Claude Code


Summary by cubic

Fixes the python-flask and python-aiohttp generators to match how Connexion passes OpenAPI 3 form request bodies: one dict argument named by x-body-name (default body), with file fields staying separate. Required form fields no longer fail every request with a 500, and optional fields are no longer silently dropped.

  • Generated handlers declare that dict argument and unpack each form field into the same local variable names as before, so the rest of the template code is unchanged.
  • Field wire names are emitted as escaped Python string literals and escaped again in docstring references, so names containing quotes, backslashes, or control characters generate valid, loadable code.
  • Non-file parameters that collide with the dict argument name are dropped from the signature and docstring with a warning, since Connexion overwrites them; a same-named file field wins and the dict is read defensively.
  • The collapsed fields' names, types, and descriptions fold into the dict argument's docstring line; file, header, and cookie docs are unchanged.
  • Webhook operations now look up their requestBody, so x-body-name and required are honored there too.
  • python-blueplanet (Connexion 1.x) keeps one argument per form field.
  • Tests cover required/optional bodies, requestBody- and schema-level x-body-name (Connexion 2 only), file + fields, parameter collisions, webhooks, a case-insensitive media type, and blueplanet's per-field arguments.

Known Connexion-side limitations remain: GET/DELETE form bodies are not passed, and file fields only arrive when their property name is a valid snake_case Python name.

Written for commit e5f9c6e. Summary will update on new commits.

View guided diff

…es the way Connexion passes them

Connexion (2.x for flask and aiohttp, and 3.x) passes an OpenAPI 3 form
request body (application/x-www-form-urlencoded or multipart/form-data)
to the handler as one argument holding the non-file fields, already
type-coerced, named by the requestBody's x-body-name (default 'body').
Binary file fields are passed as separate arguments. The generated
handlers declared one argument per form field instead. Any operation
with a required form field therefore failed with TypeError (500), and
optional fields were silently dropped. This includes the petstore
samples' update_pet_with_form and upload_file.

For operations that consume form content (the media type is compared
case-insensitively), the non-file form fields are now collapsed into
that single argument. The controller templates first copy it to a
collision-safe local (_form_body), then unpack each field into the same
local variable names as before, looked up by its wire name, so the
following template code (e.g. date conversion) keeps working. File
fields stay separate arguments. The docstring shows the argument as
:type ...: dict. python-blueplanet (Connexion 1.x) is unchanged.

The argument name is the requestBody's x-body-name, then, only for
Connexion 2, the schema-level x-body-name it still reads (Connexion 3
ignores it), then 'body'.

Known limitations, on Connexion's side: Connexion 2 doesn't pass form
bodies of GET/DELETE operations, and file fields are only passed when
their property name is already a valid snake_case Python name. An
x-body-name that is a Python builtin or 'callback' matches Connexion
once handler arguments follow Connexion's naming (OpenAPITools#25450).

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 10 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java:800">
P2: This condition also collapses legacy form parameters that do not come from an OpenAPI 3 `requestBody`. The generated handler then expects a `body` dict even though those fields are passed individually; require a resolved request body before enabling the collapse.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread modules/openapi-generator/src/main/resources/python-flask/controller.mustache Outdated
Comment thread modules/openapi-generator/src/main/resources/python-aiohttp/controller.mustache Outdated
boolean hasJsonHeaderParam = false;
for (CodegenOperation operation : operationList) {
List<CodegenParameter> handlerParams = new ArrayList<>();
boolean collapseFormBody = collapseFormBodyToDictArgument() && consumesFormContent(operation);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This condition also collapses legacy form parameters that do not come from an OpenAPI 3 requestBody. The generated handler then expects a body dict even though those fields are passed individually; require a resolved request body before enabling the collapse.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java, line 800:

<comment>This condition also collapses legacy form parameters that do not come from an OpenAPI 3 `requestBody`. The generated handler then expects a `body` dict even though those fields are passed individually; require a resolved request body before enabling the collapse.</comment>

<file context>
@@ -797,14 +797,51 @@ protected void postProcessConnexionOperationMap(OperationMap operations, Map<Str
         boolean hasJsonHeaderParam = false;
         for (CodegenOperation operation : operationList) {
             List<CodegenParameter> handlerParams = new ArrayList<>();
+            boolean collapseFormBody = collapseFormBodyToDictArgument() && consumesFormContent(operation);
+            CodegenParameter formBodyParam = null;
+            String formDictLocal = null;
</file context>
Suggested change
boolean collapseFormBody = collapseFormBodyToDictArgument() && consumesFormContent(operation);
boolean collapseFormBody = collapseFormBodyToDictArgument()
&& getOperationRequestBody(operation) != null
&& consumesFormContent(operation);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I checked this, and I don't think a separate case exists here. The generator always writes an OpenAPI 3 spec for Connexion, and Swagger 2 formData parameters become a requestBody. I generated python-flask from modules/openapi-generator/src/test/resources/2_0/petstore.yaml: the generated openapi.yaml describes updatePetWithForm with a requestBody of application/x-www-form-urlencoded. Connexion therefore passes the fields as one dict, so collapsing them is correct there too, and def update_pet_with_form(pet_id, body=None) is what Connexion calls. Leaving this as is.

khayashi4337 and others added 3 commits October 29, 2026 16:39
…ion-form-body

# Conflicts:
#	modules/openapi-generator/src/test/java/org/openapitools/codegen/python/AbstractPythonConnexionServerCodegenTest.java
…stent with what Connexion passes

Connexion passes the form body dict in a single argument whose name comes
from the requestBody x-body-name (or 'body'), overwriting any same-named
query/path argument. Non-file arguments colliding with that name are now
dropped from the signature and docstring with a warning, a same-named file
field is kept (it wins) with a warning, and the form dict initializer uses
an isinstance() check when a colliding name can still deliver a non-dict
value. Form field wire names are emitted as escaped Python string literals
through a new vendor extension rendered with triple braces, so names with
quotes, backslashes, or control characters produce valid code. Webhook
operations now resolve their requestBody too, so x-body-name and required
are honored there.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…nc with collapsed arguments

The collapsed form fields are not handler arguments anymore, so their
:param/:type docstring entries lied about the signature. Fold each
field's wire name, type and description into the dict argument's
:param line instead, and keep file fields documented as before.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 13 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

…psed form field docstrings

A form field whose wire name contains an ASCII control character (e.g. a
NUL byte) made docstringFieldName emit the raw byte into the generated
controller's docstring, leaving source Python cannot load. Escape the
whole C0 range and DEL as \xNN, matching pythonStringLiteral, and cover
both the docstring and the .get() literal with a control-named field.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant