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
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions