Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions python/composio/utils/schema_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,11 @@ def _handle_toplevel_combiner(
allow_undefined_array_items=True,
allow_undefined_type=True,
)
# A null-only anyOf/oneOf (e.g. {"anyOf": [{"type": "null"}]}) collapses to
# NoneType in the library, which loses the nullable-but-untyped intent.
# Mirror the direct {"type": "null"} mapping and return Optional[Any].
if result is type(None):
return PYDANTIC_TYPE_TO_PYTHON_TYPE["null"]
# If result is a type (like a Union or Optional), return it directly
# If result is a model class, return it
return result
Expand Down Expand Up @@ -270,6 +275,10 @@ def _build_union_from_options(options: t.List[t.Dict[str, t.Any]]) -> t.Type:
pydantic_types.append(ptype)

if len(pydantic_types) == 0:
# Preserve nullability when every branch was a null branch
# (e.g. {"anyOf": [{"type": "null"}]}) instead of downgrading to str.
if has_null:
return t.cast(t.Type, PYDANTIC_TYPE_TO_PYTHON_TYPE["null"])
return str # Fallback

if len(pydantic_types) == 1:
Expand Down
29 changes: 29 additions & 0 deletions python/tests/test_schema_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,35 @@ def test_anyof_single_type(self):
assert result is str
assert not hasattr(result, "__origin__")

@pytest.mark.unit
@pytest.mark.schema
def test_anyof_null_only_preserves_nullability(self):
"""
Regression test for #3171: anyOf containing only {"type": "null"}
must yield Optional[Any], matching the direct {"type": "null"} mapping.
Previously this was downgraded to str (or NoneType depending on the
underlying library version), erasing nullability.
"""
json_schema = {"anyOf": [{"type": "null"}]}
result = json_schema_to_pydantic_type(json_schema)
assert result == t.Optional[t.Any]

@pytest.mark.unit
@pytest.mark.schema
def test_oneof_null_only_preserves_nullability(self):
"""oneOf containing only {"type": "null"} should also yield Optional[Any]."""
json_schema = {"oneOf": [{"type": "null"}]}
result = json_schema_to_pydantic_type(json_schema)
assert result == t.Optional[t.Any]

@pytest.mark.unit
@pytest.mark.schema
def test_anyof_repeated_null_branches(self):
"""Multiple null branches still collapse to a single Optional[Any]."""
json_schema = {"anyOf": [{"type": "null"}, {"type": "null"}]}
result = json_schema_to_pydantic_type(json_schema)
assert result == t.Optional[t.Any]

def test_allof_single_option(self):
"""Test allOf with single option."""
json_schema = {
Expand Down
Loading