[python] fix: explode object query parameters
Bug
A query parameter whose schema is an object, with style/explode left at their defaults (form, true), must go on the wire as one parameter per entry, keyed by the property name alone. The python client JSON encoded the whole object into one parameter instead, both for a free-form object or map and for an object with declared properties (a model).
parameters:
- in: query
name: filter
schema:
type: object
called with {"category": "books", "createdDate:gte": "2023-01-01"}:
| on the wire | |
|---|---|
| expected | category=books&createdDate%3Agte=2023-01-01 |
| python before | filter=%7B%22category%22:%20%22books%22,%20...%7D |
Series
One of six per-language PRs for the same bug: #24797 go, #24802 python, #24803 typescript-fetch, #24867 kotlin, #24868 dart, #24869 ruby. No shared main/ code; each adds the same fixture, 3_0/exploded-object-query-param.yaml.
Fix
- Maps explode. A form-style, exploded map parameter goes through a new
ApiClient.explode_query_object(name, obj), one call per parameter inapi.mustache. It serializes the value withsanitize_for_serializationand returns one(name, value)pair per entry. - Models explode too. Because the value is serialized first, a model explodes under its wire names (
photoUrls, notphoto_urls) and unset properties are left out. A oneOf holding a primitive stays one parameter; one holding a list repeats the parameter name per item. - None is skipped. A
Noneentry or list item is left out instead of going out ask=None. An entry holding a list repeats its name per item. - Names are quoted.
parameters_to_url_queryquoted values but not names, and exploded names are runtime data. Declared names are quoted too; one with reserved characters, such aspage[size], now goes out aspage%5Bsize%5D, which servers decode to the same name. - Collection formats apply only to lists. An exploded name can match a declared array parameter: petstore's
languagemap sits next tocontext: multi, solanguage={"context": "en"}went out ascontext=e&context=n. The format now applies only when the value is a list or tuple.
Verified
A client generated from the fixture and the echo_api sample, against a server that echoes the request line:
| input | on the wire |
|---|---|
filter={"category": "books", "createdDate:gte": "2023-01-01"} | category=books&createdDate%3Agte=2023-01-01 |
typedFilter={"category": "books"} | category=books |
filter={"k": None, "a": "b", "l": ["x", None, 2]} | a=b&l=x&l=2 |
Pet(name="Hello World", photoUrls=["http://a.com"]) | name=Hello%20World&photoUrls=http%3A//a.com |
deepFilter / flatFilter | one JSON parameter (as on master) |
declared page[size] | page%5Bsize%5D=5 |
Tests: PythonClientCodegenTest#testExplodedObjectQueryParameter and #testExplodedModelQueryParameter (python-only fixture 3_0/python/exploded-model-query-param.yaml); the echo_api runtime tests test_query_style_form_explode_true_object* in samples/client/echo_api/python/tests/test_manual.py; and test_explode_query_object* and test_parameters_to_url_query_* in samples/openapi3/client/petstore/python/tests/test_api_client.py.
Known gaps
deepObject and explode: false still go out as a single JSON parameter; the spec wants deepFilter[category]=books and flatFilter=category,books. Both are pre-existing and left for a follow-up.
An entry whose value is itself an object (a nested object, or a list of objects) goes out as one JSON-encoded parameter under the entry's name. OpenAPI doesn't define how form/explode serializes nested objects, so it's left as it is.
PR checklist
- Read the contribution guidelines.
- Built the project and updated samples (
./bin/generate-samples.sh bin/configs/python*.yaml): 15 generated files,api_client.py(the helper, quoting and guard) andfake_api.py/query_api.py(petstore'slanguagemap and the echo API's models). Thepython-pydantic-v1samples are untouched; that generator has its ownapi_client.mustache. - Technical committee: @cbornet @tomplus @arun-nalla
Generated with Claude Code