Skip to content

null-only anyOf schemas are downgraded to str instead of preserving nullability #3171

@YizukiAme

Description

@YizukiAme

Bug Description

json_schema_to_pydantic_type() correctly maps a direct {"type": "null"} schema to Optional[Any], and it already preserves boolean|null or object|null unions. But when a top-level combiner collapses to only a null branch, the helper falls into its generic string fallback.

_build_union_from_options() marks the null branch with has_null=True, then returns str when no non-null branches remain. The earlier filtered_schema is None -> return str fallback does the same after boolean-schema filtering.

Schemas such as {"anyOf": [{"type": "null"}]} or {"anyOf": [false, {"type": "null"}]} are therefore converted into str-typed fields instead of nullable ones. Any model builder that delegates combiners to this helper then emits type: string for a schema that only permits null, and explicit None values are rejected by Pydantic.

Root Cause

python/composio/utils/schema_converter.py L146-149, L257-273:

filtered_schema = _filter_boolean_schemas(json_schema)
if filtered_schema is None:
    return str  # Fallback if all schemas were false
...
for option in options:
    ptype = json_schema_to_pydantic_type(option)
    if ptype is None:
        continue
    if ptype == null_type or ptype is type(None):
        has_null = True
        continue
    pydantic_types.append(ptype)

if len(pydantic_types) == 0:
    return str  # Fallback — should be Optional[Any] when has_null=True

Steps to Reproduce

from composio.utils.schema_converter import json_schema_to_pydantic_type

# Direct null schema — works correctly
result = json_schema_to_pydantic_type({"type": "null"})
print(result)  # Optional[Any] ✓

# Null-only anyOf — falls back to str instead of Optional[Any]
result = json_schema_to_pydantic_type({"anyOf": [{"type": "null"}]})
print(result)  # str ✗ — should be Optional[Any]

Expected Behavior

When the only remaining branch in an anyOf/oneOf is null, the resulting type should be Optional[Any], not str.

Suggested Fix

When combiner processing sees at least one null branch and no remaining concrete branches, return Optional[Any] instead of str:

if len(pydantic_types) == 0:
    return Optional[Any] if has_null else str

Discovered during code review.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingpy

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions