#2194·iii

format_utils._to_json_schema silently returns None for bare dict / Optional[dict] (and bare list)

Author: 0xm0Created Sep 17, 2026Updated Sep 17, 2026

Summary

iii.format_utils._to_json_schema returns None for a bare dict annotation, and for Optional[dict]. Parameterized or typing-alias forms work. The failure is silent: the schema for the parameter comes out empty, so a capability/worker author typing a parameter Optional[dict] gets no schema for that parameter at all instead of {"type": ["object", "null"]}.

The same defect class also affects bare list and Optional[list] (see repro table).

Root cause

sdk/packages/python/iii/src/iii/format_utils.py, _to_json_schema (line 42):

python
origin = get_origin(annotation)
...
if origin is list:        # line 66
...
if origin is dict:        # line 76

For a bare class (dict, list), typing.get_origin returns None — the class itself is not a parameterized alias — so neither branch fires and control falls through to return None. typing.Dict / typing.List work even unparameterized because they are generic aliases and carry origin is dict / origin is list.

I confirmed the current main branch carries the identical keying (if origin is dict: at line 76 of sdk/packages/python/iii/src/iii/format_utils.py as of 2026-09-17).

Repro

iii-sdk 0.20.0, Python 3.14.5:

python
import typing
from iii import format_utils as fu

for ann in (typing.Optional[dict], dict,
            typing.Optional[dict[str, typing.Any]], typing.Optional[typing.Dict],
            list, typing.Optional[list]):
    print(repr(ann), '->', fu._to_json_schema(ann))

Output:

dict | None -> None                                    # WRONG — want {"type": ["object", "null"]}
<class 'dict'> -> None                                 # WRONG — want {"type": "object"}
dict[str, typing.Any] | None -> {'type': ['object', 'null']}
typing.Dict | None -> {'type': ['object', 'null']}
<class 'list'> -> None                                 # WRONG — want {"type": "array"}
list | None -> None                                    # WRONG — want {"type": ["array", "null"]}

Impact

Any module author who types a parameter Optional[dict] (the natural way to write "an optional object") gets a silently-empty output schema for that parameter instead of {type: [object, null]}. In the RapidEngine conductor's rapid packaging (which consumes _to_json_schema), _schema_for(Optional[dict]) emits {} — the parameter vanishes from the module descriptor with no warning at generation time.

Suggested fix

Key the branches on the class, not the alias origin — e.g.:

python
origin = get_origin(annotation)
if origin is list or annotation is list: ...
if origin is dict or annotation is dict: ...

(or normalize bare list/dict to their parameterized typing aliases before the origin checks — that also preserves additionalProperties handling for parameterized forms).

Bug observed in iii-sdk 0.20.0; I verified format_utils.py is byte-identical in the 0.23.0 wheel (159 lines, both versions) and the current main branch still keys if origin is dict:.


Environment: iii-sdk 0.20.0 (byte-identical in 0.23.0 wheel and on main), Python 3.14.5, macOS (darwin 25.4).