Skip to content

fix: preserve nullability for null-only anyOf/oneOf schemas (#3171)#3313

Closed
zen-agent wants to merge 1 commit into
nextfrom
zen/fix-3171-null-only-anyof-rpt29y
Closed

fix: preserve nullability for null-only anyOf/oneOf schemas (#3171)#3313
zen-agent wants to merge 1 commit into
nextfrom
zen/fix-3171-null-only-anyof-rpt29y

Conversation

@zen-agent
Copy link
Copy Markdown
Contributor

Description

Fixes #3171.

json_schema_to_pydantic_type() in python/composio/utils/schema_converter.py correctly maps a direct {"type": "null"} schema to Optional[Any] (via PYDANTIC_TYPE_TO_PYTHON_TYPE), but a null-only anyOf like {"anyOf": [{"type": "null"}]} was being downgraded — to bare NoneType (when the underlying library resolves the combiner) or str (when execution reached the manual _build_union_from_options fallback). Both outcomes erase the nullable-but-untyped intent.

Root cause

Two paths hit the same gap:

  1. _handle_toplevel_combinerjson_schema_to_pydantic library succeeds and returns type(None) for a null-only combiner, so we never reach the manual fallback.
  2. _build_union_from_options — when every option is filtered into the has_null=True bucket and pydantic_types ends up empty, the function falls through to the generic return str instead of honoring has_null.

Fix

  • _build_union_from_options: when has_null=True and no non-null branches remain, return Optional[Any] instead of str.
  • _handle_toplevel_combiner: when the library resolves the combiner to bare NoneType, normalize it to Optional[Any] so it matches the direct {"type": "null"} mapping.

How did I test this PR

  • Added 3 regression tests in tests/test_schema_parser.py::TestJsonSchemaToPydanticType:
    • test_anyof_null_only_preserves_nullability{"anyOf": [{"type": "null"}]}Optional[Any]
    • test_oneof_null_only_preserves_nullability{"oneOf": [{"type": "null"}]}Optional[Any]
    • test_anyof_repeated_null_branches — multiple null branches collapse cleanly
  • Ran the full schema parser suite — 68 passed, no regressions:
    cd python && source .venv/bin/activate
    python -m pytest tests/test_schema_parser.py -v
    
  • Manually verified the fix preserves existing mixed-type behavior:
    • {"anyOf": [{"type": "string"}, {"type": "null"}]}Optional[str]
    • {"anyOf": [{"type": "string"}, {"type": "integer"}]}Union[str, int]
    • {"type": "null"}Optional[Any] ✓ (unchanged)

Triggered by: abhishek@composio.dev | Source: slack
Session: https://zen-api-production-4c98.up.railway.app/dashboard/#/chat/zen-e25dc264c721

`json_schema_to_pydantic_type` correctly maps a direct `{"type": "null"}`
schema to `Optional[Any]`, but a null-only `anyOf` (e.g.
`{"anyOf": [{"type": "null"}]}`) was collapsing to either `NoneType`
(via the underlying library) or `str` (via the manual fallback in
`_build_union_from_options`), erasing the nullable intent.

Fixes:
- `_build_union_from_options`: when `has_null=True` and no non-null
  branches remain, return `Optional[Any]` instead of falling back to
  `str`.
- `_handle_toplevel_combiner`: when the library successfully resolves
  the combiner to bare `NoneType`, normalize it to `Optional[Any]`
  for consistency with the direct `{"type": "null"}` mapping.

Adds regression tests covering null-only `anyOf`, null-only `oneOf`,
and repeated null branches.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Co-authored-by: Abhishek <abhishek@composio.dev>
@zen-agent zen-agent requested a review from jkomyno as a code owner April 29, 2026 14:03
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Apr 29, 2026 2:04pm

Request Review

@zen-agent
Copy link
Copy Markdown
Contributor Author

Post-PR status

  • build-checks (scoped):ruff check + ruff format --check clean on python/composio/utils/schema_converter.py and python/tests/test_schema_parser.py. The two repo-wide build-checks failures (docs/examples/workplace-search/authorize.py:27 f-string lint, and pytest collection errors for missing optional agents/claude_agent_sdk packages) reproduce on origin/next and are unrelated to this PR.
  • Codex review: ✅ LGTM — "The changes are narrowly scoped to preserving the existing null type mapping for null-only combiners and include regression coverage for anyOf/oneOf cases. I did not find any introduced correctness issues in the diff."
  • CI: ✅ All 10 checks green — Lint and Format Check, Test Python SDK (3.10/3.11/3.12), Integration Tests, Test Circular Import Prevention, Detect Secrets with Gitleaks, common-checks, Vercel.
  • Tests:
    • 3 new unit tests in TestJsonSchemaToPydanticType covering null-only anyOf, null-only oneOf, and repeated null branches.
    • Full tests/test_schema_parser.py suite: 68/68 passed locally and in CI across Python 3.10/3.11/3.12.
    • E2E/runtime not applicable — this is a pure schema-conversion utility with no Apollo/Thermos endpoint surface.
  • Review comments: None to address — only the automated Vercel preview deploy bot.

Ready for review/merge.

@jkomyno jkomyno closed this May 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants