microsoft / microsoft/semantic-kernel
Python: KernelJsonSchemaBuilder ignores string forward references inside list[...]/dict[...], emitting a bare {"type": "object"}
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 28.6k
- Forks
- 4.8k
- Avg merge
- 14h 13m
- Merged PRs (30d)
- 18
Description
Describe the bug
KernelJsonSchemaBuilder silently drops the element schema when a container's element type is written as a string forward reference. list["Inner"] produces {"type": "object"} for the items — no properties, no required — while list[Inner] produces the full schema.
That schema is what gets sent to the model as a function-calling parameter definition, so a plugin using this annotation style hands the model an untyped blob for that argument.
To Reproduce
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.schema.kernel_json_schema_builder import KernelJsonSchemaBuilder as B
class Inner(KernelBaseModel):
value: int
label: str
class HolderDirect(KernelBaseModel):
items: list[Inner] = []
class HolderFwd(KernelBaseModel):
items: list["Inner"] = []
class HolderTop(KernelBaseModel):
one: "Inner"
| model | items / one schema |
|---|---|
HolderDirect |
{"type": "array", "items": {"type": "object", "properties": {"value": ..., "label": ...}, "required": [...]}} |
HolderFwd |
{"type": "array", "items": {"type": "object"}} |
HolderTop |
full Inner schema |
dict[str, "Inner"] |
additionalProperties: {"type": "object", "properties": {}} |
HolderTop works and HolderFwd doesn't, which is the part that makes this easy to miss — forward references look supported.
Why
HolderFwd.__annotations__["items"] -> list['Inner']
get_type_hints(HolderFwd)["items"] -> list['Inner'] # unchanged
get_args(...) -> ('Inner',) # a str, not a type
HolderFwd.model_fields["items"].annotation -> list['Inner'] # pydantic doesn't resolve it either
get_type_hints evaluates an annotation that is a string. It does not descend into a generic alias that already exists as an object and evaluate strings sitting in its __args__. list["Inner"] goes through list.__class_getitem__, which stores "Inner" verbatim — no ForwardRef wrapper, so there's nothing for get_type_hints to resolve. typing.Optional["Inner"] does wrap the string in a ForwardRef, which is why the top-level and Optional forms work.
handle_complex_type then calls cls.build("Inner", ...), which takes the isinstance(parameter_type, str) branch at the top of build and lands in build_from_type_name. No entry matches "Inner", so it returns the {"type": "object"} fallback:
KernelJsonSchemaBuilder.build("Inner") # -> {'type': 'object'}
Nothing raises, and {"type": "object"} is indistinguishable from a genuinely-unknown type downstream.
Expected behavior
list["Inner"] and list[Inner] produce the same schema.
Resolving str args against the owning model's module globals inside handle_complex_type before recursing would do it — the module globals are already fetched a few lines up in build_model_schema for the get_type_hints call.
Platform
- OS: Linux
- Python: 3.10.12
- Semantic Kernel:
main@ 7d885dd (python)
Additional context
Separate from the recursion work in #14198. That PR is about cycles; this fires on a plain non-recursive list["Inner"]. It's also the reason two of #14198's new tests currently fail — TreeNode and Author/Book reach their cycles through list[...], so the string never resolves to a class and the cycle detection never engages.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in semantic_kernel/schema/kernel_json_schema_builder.py, especially build_model_schema and handle_complex_type; inspect how container args are passed into build. Reproduce the difference between list["Inner"] and list[Inner] using the issue's Holder examples, then add coverage showing equivalent item and additionalProperties schemas.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100