Repository navigation
feat(python-flask, python-aiohttp): deserialize object-typed query and header parameters #25427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
wing328
merged 18 commits into
OpenAPITools:master
from
khayashi4337:feat/python-connexion-object-params
Nov 2, 2026
+796
−26
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
d4193db
fix(python-aiohttp, python-blueplanet, python-flask): read header par…
khayashi4337 910dcae
fix(python-aiohttp, python-blueplanet, python-flask): handle cookie p…
khayashi4337 ec1eeee
fix(python-aiohttp, python-blueplanet, python-flask): format array de…
khayashi4337 cf71763
fix(python-aiohttp, python-blueplanet, python-flask): guard None date…
khayashi4337 ad058d0
Merge remote-tracking branch 'upstream/master' into fix/python-connex…
khayashi4337 9e43103
fix(python-aiohttp, python-blueplanet, python-flask): don't call obje…
khayashi4337 cd3fe31
Merge remote-tracking branch 'upstream/master' into fix/python-connex…
khayashi4337 a82ad55
feat(python-flask, python-aiohttp): deserialize object-typed query an…
khayashi4337 3b371f1
Merge remote-tracking branch 'upstream/master' into feat/python-conne…
khayashi4337 28a56b3
fix(python-flask, python-aiohttp): address review of object-typed par…
khayashi4337 129c4fd
Merge remote-tracking branch 'upstream/master' into feat/python-conne…
khayashi4337 2217b4b
Merge remote-tracking branch 'upstream/master' into feat/python-conne…
khayashi4337 3224b5e
Carry serialized query pairs into x-test-query-string
khayashi4337 2ed95e1
Accept type-less and singleton-array object schemas
khayashi4337 d033425
Emit no pairs for objects with no serializable scalar properties
khayashi4337 fe34c94
Drop unencodable array-of-object header param and guard nested props
khayashi4337 0fc610e
Keep strict object-type gate, restore X-Obj-List, skip unencodable tests
khayashi4337 9e316da
Render test query-pair names unescaped in flask and aiohttp tests
khayashi4337 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
320 changes: 315 additions & 5 deletions
320
...rc/main/java/org/openapitools/codegen/languages/AbstractPythonConnexionServerCodegen.java
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 7 additions & 2 deletions
9
modules/openapi-generator/src/main/resources/python-aiohttp/__init__main.mustache
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
modules/openapi-generator/src/main/resources/python-aiohttp/param_parsing.mustache
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| """Deserialize object-typed query and header parameters. | ||
|
|
||
| Connexion only turns ``style: deepObject`` query parameters into objects. A | ||
| ``style: form`` query parameter (``?name=a&size=3``) or a ``style: simple`` | ||
| header (``X-Trace: id,abc``) whose schema is an object reaches validation as | ||
| missing or as a plain string, so valid requests are rejected with 400. The | ||
| classes below rebuild those objects before Connexion validates them, following | ||
| https://spec.openapis.org/oas/v3.0.3#style-values. | ||
| """ | ||
| try: # Connexion 3 | ||
| from connexion.uri_parsing import OpenAPIURIParser | ||
| from connexion.validators.parameter import ParameterValidator | ||
| except ImportError: # Connexion 2 | ||
| from connexion.decorators.uri_parsing import OpenAPIURIParser | ||
| from connexion.decorators.validation import ParameterValidator | ||
|
|
||
|
|
||
| _SCALAR_TYPES = ('string', 'integer', 'number', 'boolean') | ||
|
|
||
|
|
||
| def _is_object(param): | ||
| """An object schema whose properties are all scalars; these are the only objects that | ||
| ``style: form``/``simple`` can carry. Must match the generator's isScalarObjectSchema().""" | ||
| schema = param.get('schema', {}) | ||
| properties = schema.get('properties') | ||
| if (not properties or schema.get('type') != 'object' | ||
| or any(key in schema for key in ('allOf', 'oneOf', 'anyOf'))): | ||
| return False | ||
| return all(prop.get('type') in _SCALAR_TYPES for prop in properties.values()) | ||
|
|
||
|
|
||
| def _cast(value, target): | ||
| """Cast a serialized scalar to ``target`` (int, float or bool); leave it unchanged otherwise.""" | ||
| if not isinstance(value, str): | ||
| return value | ||
| if target is bool: | ||
| lowered = value.lower() | ||
| return lowered == 'true' if lowered in ('true', 'false') else value | ||
| if target in (int, float): | ||
| try: | ||
| return target(value) | ||
| except ValueError: | ||
| return value | ||
| return value | ||
|
|
||
|
|
||
| _SCHEMA_TYPES = {'integer': int, 'number': float, 'boolean': bool} | ||
|
|
||
|
|
||
| def _cast_by_schema(obj, schema): | ||
| properties = schema.get('properties', {}) | ||
| return {key: _cast(value, _SCHEMA_TYPES.get(properties.get(key, {}).get('type'))) | ||
| for key, value in obj.items()} | ||
|
|
||
|
|
||
| def parse_simple_object(value, explode=False, model=None): | ||
| """Parse a ``style: simple`` (or non-exploded ``style: form``) object. | ||
|
|
||
| ``'k1,v1,k2,v2'`` when ``explode`` is false, ``'k1=v1,k2=v2'`` when true. | ||
| When ``model`` is given, values are cast to the types of its attributes. | ||
| """ | ||
| if not isinstance(value, str): | ||
| return value | ||
| parts = value.split(',') if value else [] | ||
| # A malformed value is returned unchanged, so that validation rejects it instead of | ||
| # silently dropping part of the input. | ||
| if explode: | ||
| if any('=' not in part for part in parts): | ||
| return value | ||
| obj = dict(part.split('=', 1) for part in parts) | ||
| else: | ||
| if len(parts) % 2: | ||
| return value | ||
| obj = dict(zip(parts[0::2], parts[1::2])) | ||
| if model is not None: | ||
| instance = model() # openapi_types/attribute_map are set per instance | ||
| types = {instance.attribute_map[attr]: t for attr, t in instance.openapi_types.items()} | ||
| obj = {key: _cast(item, types.get(key)) for key, item in obj.items()} | ||
| return obj | ||
|
|
||
|
|
||
| class ObjectParameterURIParser(OpenAPIURIParser): | ||
| """Collects object-typed ``style: form`` query parameters into a single value.""" | ||
|
|
||
| def resolve_query(self, query_data): | ||
| original = dict(query_data) | ||
| query_data = dict(query_data) | ||
| consumed = set() | ||
| for name, param in self.param_defns.items(): | ||
| if param.get('in') != 'query' or not _is_object(param) or param.get('style', 'form') != 'form': | ||
| continue | ||
| if param.get('explode', True): | ||
| # ?name=abc&size=3 -> {'name': 'abc', 'size': '3'}; a property may share the parameter's name. | ||
| properties = param.get('schema', {}).get('properties', {}) | ||
| picked = {key: original[key][-1] for key in properties if key in original} | ||
| if picked: | ||
| query_data[name] = [picked] | ||
| consumed.update(picked) | ||
| elif name in original: | ||
| query_data[name] = [parse_simple_object(original[name][-1])] | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
|
||
| # Drop the property keys we consumed so strict_validation does not report them as unknown, | ||
| # but keep any that are parameters of their own. | ||
| for key in consumed: | ||
| if key not in self.param_defns: | ||
| query_data.pop(key, None) | ||
| return super().resolve_query(query_data) | ||
|
|
||
|
|
||
| class ObjectParameterValidator(ParameterValidator): | ||
| """Parses object-typed ``style: simple`` headers before validating them.""" | ||
|
|
||
| def validate_header_parameter(self, param, request): | ||
| value = request.headers.get(param['name']) | ||
| if value is not None and _is_object(param): | ||
| # Connexion 3 validates header values as they are (no type coercion), so cast the leaves here; | ||
| # Connexion 2 would coerce them itself, and casting twice is harmless. | ||
| value = parse_simple_object(value, param.get('explode', False)) | ||
| if isinstance(value, dict): # a malformed value stays a string and fails validation | ||
| value = _cast_by_schema(value, param.get('schema', {})) | ||
| return self.validate_parameter('header', value, param) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.