feat(python-flask, python-aiohttp): deserialize object-typed query and header parameters
Builds on #25362 (merged), which makes the handlers read header parameters from the request.
Problem
OpenAPI 3 lets a query or header parameter be an object (Style Values):
| location | style | wire format |
|---|---|---|
| query | form (default, explode: true) | ?name=abc&size=3 |
| query | form, explode: false | ?filter=name,abc,size,3 |
| query | deepObject | ?filter[name]=abc&filter[size]=3 |
| header | simple (default) | X-Trace: id,abc,hops,3 |
Connexion (2.14.2 and 3.3.0; I checked 1.1.15 to 2.10.0 by reading their source too) only builds objects for deepObject. For form the parameter never reaches the handler. A simple header is validated as a plain string, so a valid request is rejected with 400 ("'id,abc' is not of type 'object'"). The generated controller tests also send the model's repr, so they cannot pass either.
Changes
Only applied when the spec has such parameters, i.e. a type: object model whose properties are all scalars (string, integer, number, boolean, including enums and dates), used as a query (form/deepObject) or header (simple) parameter. style: form/simple cannot carry nested objects or arrays, so models with such properties keep the previous handling. Specs without them generate exactly the same output as before.
- New supporting file
param_parsing.py(python-flask and python-aiohttp, same content).ObjectParameterURIParsercollectsformobject query parameters, both exploded and not.ObjectParameterValidatorparsessimpleobject headers before validation. Both plug into Connexion's own extension points,uri_parser_classandvalidator_map, which exist with the same classes in Connexion 2 and 3. - They are registered in
__main__.py/test/__init__.py(flask, Connexion 2 anduseConnexion3) and in__init__.py/tests/conftest.py(aiohttp). - Handlers: Connexion never passes header parameters to the handler (#25362 reads them from the request), so an object header is parsed with
param_parsing.parse_simple_object(..., model=Trace). Values are cast to the model attribute types, so that"false"does not becomeTrue. - Generated controller tests send these parameters in their style. They use the spec's explicit object example (parameter
example/examples, then the schema'sexample/default) and otherwise build one from the property examples. For the comma-separated styles, a value containing,or=is avoided, because OpenAPI defines no escaping for them. - A malformed value (an odd number of tokens, or a segment without
=when exploded) is left unchanged, so validation rejects it with 400 instead of silently dropping input. - Header and cookie names, and query keys, are written as Python string literals without HTML escaping (
x-python-connexion-quoted-name). The generated test and the handler'sheaders.get()/cookies.get()therefore use the same name. - Not covered, so they keep the previous handling: composed schemas (
allOf/oneOf/anyOf), models withouttype: object(Connexion itself raisesKeyError: 'type'for such query parameters),pipeDelimited/spaceDelimited, and parameters serialized withcontent(JSON headers stay on #25362'sjson.loadspath). python-blueplanet (Connexion 1.x / Swagger 2.0) opts out throughsupportsObjectParameterParsing().
Verification
Python*Test/AbstractPython*Testpass. New tests cover flask (Connexion 2 and 3) and aiohttp, plus a petstore check that noparam_parsingis emitted.- Generated from a runtime variant of
3_0/python-flask/object-typed-params.yamlwithout the unsupported parameters, and ran the generated controller test with handler assertions thatFilter/Traceinstances with the right typed values (str, int, booltrue/false, enum, date, explicit example) arrive. The full fixture's generated test also hasopenapi_server.Composed()etc. for the unsupported parameters, which raisesNameErroruntil #25426 is merged.- python-flask + Connexion 2.14.2: passed
- python-flask (
useConnexion3) + Connexion 3.3.0: passed - python-aiohttp + Connexion 2.14.2: passed. Date properties stay strings here, because the aiohttp
requirements.txthas nopython_dateutil; that is pre-existing and not changed in this PR.
strict_validation=Trueon Connexion 2 and 3: the same request returns 200, and an unknown parameter is still rejected with 400.- Malformed values (
X-Trace: unexpected,?packed=name,abc,size) get 400 on Connexion 2 and 3. bin/generate-samples.shforbin/configs/python-{flask,flask-connexion3,aiohttp,aiohttp-srclayout,blueplanet}.yamlproduces no diff.
No existing sample has object-typed query/header parameters, so CI does not execute param_parsing.py. If you'd like, I can add a small sample config for it to the Python server workflow.
PR checklist
- Read the contribution guidelines.
- Run the following to build the project and update samples:
Regenerated the samples for the affected generators: no diff. Generator options and docs are unchanged../mvnw clean package || exit ./bin/generate-samples.sh ./bin/configs/*.yaml || exit ./bin/utils/export_docs_generators.sh || exit - 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 (Claude Code, with reviews by Codex and Fable) and reviewed and verified by me.
🤖 Generated with Claude Code