Bug Report Checklist
Description
The Python generator writes each operation's _response_types_map from {{dataType}} — the Python type annotation for the response. But ApiClient.__deserialize consumes those strings with a three-rule grammar that is not Python:
if klass.startswith('List['): m = re.match(r'List\[(.*)]', klass) # recurse on group 1
if klass.startswith('Dict['): m = re.match(r'Dict\[([^,]*), (.*)]', klass) # recurse on group 2
if klass in self.NATIVE_TYPES_MAPPING: ... # int/str/object/UUID/…
else: klass = getattr(models, klass) # anything else
Optional[...] matches none of those three rules. So when a response type is nullable, the generator emits a string its own runtime cannot parse: the Dict[ rule matches, recursion reaches Optional[object], and that falls through to getattr(models, "Optional[object]").
Every successful response from the endpoint raises AttributeError, before any user code runs.
The same text is simultaneously correct and incorrect, which is the clearest way to see the defect. From one generated file:
) -> Dict[str, Optional[object]]: # line 53 — correct as an annotation
'200': "Dict[str, Optional[object]]", # line 87 — invalid as a protocol string
Emission site is modules/openapi-generator/src/main/resources/python/partial_api.mustache:
_response_types_map: Dict[str, Optional[str]] = {
{{#responses}}
{{^isWildcard}}
'{{code}}': {{#dataType}}"{{.}}"{{/dataType}}{{^dataType}}None{{/dataType}},
Actual output: AttributeError: module 'repro_api.models' has no attribute 'Optional[object]' on every 200.
Expected output: the response deserialized, i.e. {'a': 1} for a body of {"a": 1}.
openapi-generator version
Reproduced identically on 7.24.0, 7.23.0, and master (7.25.0-SNAPSHOT, build 7.25.0-20260814.055009-235).
Not a regression — all three produce byte-identical output for the specs below. This appears to be long-standing.
OpenAPI declaration file content or url
Minimal, OpenAPI 3.0.3. The trigger is additionalProperties.nullable:
{
"openapi": "3.0.3",
"info": { "title": "Repro", "version": "1.0" },
"paths": {
"/settings": {
"get": {
"operationId": "get_settings",
"responses": {
"200": {
"description": "A map whose values are nullable.",
"content": {
"application/json": {
"schema": { "type": "object", "additionalProperties": { "nullable": true } }
}
}
}
}
}
}
}
}
Removing "nullable": true emits the correct "Dict[str, object]" and works, which isolates nullability as the cause.
The OpenAPI 3.1 spelling of the same thing, "additionalProperties": { "anyOf": [{}, { "type": "null" }] }, emits "Dict[str, Optional[AnyOf]]" — same root cause, and AnyOf is not a model either. That is the shape FastAPI emits for dict[str, Any | None], so it is reachable without hand-writing a spec.
Generation Details
openapi-generator generate -g python -i repro.json -o out \
-c <(echo '{"library":"httpx","packageName":"repro_api"}')
Also reproduces with the default urllib3 library — __deserialize lives in api_client.mustache, which is shared by all library variants.
Steps to reproduce
-
Generate from the spec above.
-
Observe repro_api/api/default_api.py:
_response_types_map: Dict[str, Optional[str]] = {
'200': "Dict[str, Optional[object]]",
}
-
Call the deserializer with that string, exactly as call_api does for any 200:
from repro_api.api_client import ApiClient
c = ApiClient()
deserialize = getattr(c, '_ApiClient__deserialize')
deserialize({"a": 1}, "Dict[str, Optional[object]]")
AttributeError: module 'repro_api.models' has no attribute 'Optional[object]'
For contrast, in the same interpreter:
'Dict[str, object]' -> {'a': 1}
'Dict[str, Optional[object]]' -> AttributeError: ... has no attribute 'Optional[object]'
'Dict[str, object | None]' -> AttributeError: ... has no attribute 'object | None'
The third line matters for anyone tempted to fix this by modernising the string to PEP 604 — the grammar has no union syntax at all, so the wrapper has to be removed, not translated.
Related issues/PRs
Searched and found nothing reporting this. Adjacent but different — all concern model type generation or an empty map rather than _response_types_map carrying an annotation the deserializer cannot parse: #16967, #18774, #20373, #5421, #8227.
Originally filed on the npm wrapper repo by mistake: OpenAPITools/openapi-generator-cli#1274.
Suggest a fix
Two candidates. The second is smaller and also repairs already-generated clients on regeneration.
-
Stop using dataType as a protocol string. partial_api.mustache needs the deserializer's type language, not the annotation language — either a separate codegen property for it, or strip the nullable wrapper at that site. This addresses the cause, but any future annotation feature can reopen the same gap.
-
Teach __deserialize the wrapper. Unwrap Optional[...] (and, for robustness, a trailing | None) before the List[/Dict[ dispatch in api_client.mustache. Dropping nullability loses nothing at runtime: __deserialize already returns None for None data as its first action, and the method annotation still tells the truth to consumers and type checkers.
Note that unwrapping needs to be bracket-matched rather than regex-substituted. A pattern like Optional\[([^\[\]]*)\] only matches a wrapper whose contents hold no brackets, so List[Optional[Dict[str, Optional[int]]]] stalls after the innermost pass and leaves an Optional behind.
Happy to open a PR for whichever direction maintainers prefer.
Bug Report Checklist
Description
The Python generator writes each operation's
_response_types_mapfrom{{dataType}}— the Python type annotation for the response. ButApiClient.__deserializeconsumes those strings with a three-rule grammar that is not Python:Optional[...]matches none of those three rules. So when a response type is nullable, the generator emits a string its own runtime cannot parse: theDict[rule matches, recursion reachesOptional[object], and that falls through togetattr(models, "Optional[object]").Every successful response from the endpoint raises
AttributeError, before any user code runs.The same text is simultaneously correct and incorrect, which is the clearest way to see the defect. From one generated file:
Emission site is
modules/openapi-generator/src/main/resources/python/partial_api.mustache:Actual output:
AttributeError: module 'repro_api.models' has no attribute 'Optional[object]'on every 200.Expected output: the response deserialized, i.e.
{'a': 1}for a body of{"a": 1}.openapi-generator version
Reproduced identically on 7.24.0, 7.23.0, and master (
7.25.0-SNAPSHOT, build7.25.0-20260814.055009-235).Not a regression — all three produce byte-identical output for the specs below. This appears to be long-standing.
OpenAPI declaration file content or url
Minimal, OpenAPI 3.0.3. The trigger is
additionalProperties.nullable:{ "openapi": "3.0.3", "info": { "title": "Repro", "version": "1.0" }, "paths": { "/settings": { "get": { "operationId": "get_settings", "responses": { "200": { "description": "A map whose values are nullable.", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": { "nullable": true } } } } } } } } } }Removing
"nullable": trueemits the correct"Dict[str, object]"and works, which isolates nullability as the cause.The OpenAPI 3.1 spelling of the same thing,
"additionalProperties": { "anyOf": [{}, { "type": "null" }] }, emits"Dict[str, Optional[AnyOf]]"— same root cause, andAnyOfis not a model either. That is the shape FastAPI emits fordict[str, Any | None], so it is reachable without hand-writing a spec.Generation Details
openapi-generator generate -g python -i repro.json -o out \ -c <(echo '{"library":"httpx","packageName":"repro_api"}')Also reproduces with the default
urllib3library —__deserializelives inapi_client.mustache, which is shared by all library variants.Steps to reproduce
Generate from the spec above.
Observe
repro_api/api/default_api.py:Call the deserializer with that string, exactly as
call_apidoes for any 200:For contrast, in the same interpreter:
The third line matters for anyone tempted to fix this by modernising the string to PEP 604 — the grammar has no union syntax at all, so the wrapper has to be removed, not translated.
Related issues/PRs
Searched and found nothing reporting this. Adjacent but different — all concern model type generation or an empty map rather than
_response_types_mapcarrying an annotation the deserializer cannot parse: #16967, #18774, #20373, #5421, #8227.Originally filed on the npm wrapper repo by mistake: OpenAPITools/openapi-generator-cli#1274.
Suggest a fix
Two candidates. The second is smaller and also repairs already-generated clients on regeneration.
Stop using
dataTypeas a protocol string.partial_api.mustacheneeds the deserializer's type language, not the annotation language — either a separate codegen property for it, or strip the nullable wrapper at that site. This addresses the cause, but any future annotation feature can reopen the same gap.Teach
__deserializethe wrapper. UnwrapOptional[...](and, for robustness, a trailing| None) before theList[/Dict[dispatch inapi_client.mustache. Dropping nullability loses nothing at runtime:__deserializealready returnsNoneforNonedata as its first action, and the method annotation still tells the truth to consumers and type checkers.Note that unwrapping needs to be bracket-matched rather than regex-substituted. A pattern like
Optional\[([^\[\]]*)\]only matches a wrapper whose contents hold no brackets, soList[Optional[Dict[str, Optional[int]]]]stalls after the innermost pass and leaves anOptionalbehind.Happy to open a PR for whichever direction maintainers prefer.