[python] fix: explode object query parameters

Merged
WI
1mo ago
+387-57

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
expectedcategory=books&createdDate%3Agte=2023-01-01
python beforefilter=%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

  1. Maps explode. A form-style, exploded map parameter goes through a new ApiClient.explode_query_object(name, obj), one call per parameter in api.mustache. It serializes the value with sanitize_for_serialization and returns one (name, value) pair per entry.
  2. Models explode too. Because the value is serialized first, a model explodes under its wire names (photoUrls, not photo_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.
  3. None is skipped. A None entry or list item is left out instead of going out as k=None. An entry holding a list repeats its name per item.
  4. Names are quoted. parameters_to_url_query quoted values but not names, and exploded names are runtime data. Declared names are quoted too; one with reserved characters, such as page[size], now goes out as page%5Bsize%5D, which servers decode to the same name.
  5. Collection formats apply only to lists. An exploded name can match a declared array parameter: petstore's language map sits next to context: multi, so language={"context": "en"} went out as context=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:

inputon 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 / flatFilterone 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) and fake_api.py/query_api.py (petstore's language map and the echo API's models). The python-pydantic-v1 samples are untouched; that generator has its own api_client.mustache.
  • Technical committee: @cbornet @tomplus @arun-nalla

Generated with Claude Code