fix(python-flask, python-aiohttp): accept OpenAPI 3 form request bodies the way Connexion passes them
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
bodydoesn'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-levelx-body-namethat it still reads comes next; Connexion 3 ignores it. Otherwise the name isbody. The docstring shows it asdict. - python-blueplanet (Connexion 1.x) is unchanged.
- Regenerated the petstore samples for python-flask, python-flask (Connexion 3) and python-aiohttp (
pet_controller.pyonly). - 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)).
- 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
- 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
webhookstoo, sox-body-nameandrequiredare 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:paramline, e.g.:param body: Form fields: name (str): Updated name of the pet; status (str): Updated status of the pet.
Verification
-
Python*Test/AbstractPython*Testpass. The new tests cover flask, flask withuseConnexion3, and aiohttp: required and optional bodies, requestBody and schema-levelx-body-name, file + fields, a camelCase path parameter with a required body, a field namedbody, 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, withname='a',size=3(int) andbody='hello'delivered; - multipart
name=aplus a file: 200, withnameand the file delivered.
With the generator from master, the urlencoded request returns 500, and the multipart one returns 200 but loses
name. - urlencoded
-
bin/generate-samples.sh bin/configs/python*.yaml: the only changes are the three petstorepet_controller.pyfiles.
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-namethat is a Python builtin (e.g.input) orcallbackonly matches Connexion's naming together with #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:
Regenerated all./mvnw clean package || exit ./bin/generate-samples.sh ./bin/configs/*.yaml || exit ./bin/utils/export_docs_generators.sh || exitbin/configs/python*.yamlsamples 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-nameandrequiredare 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.