fix(python): fallback to 'GeneratedModel' when schema has no title#3325
fix(python): fallback to 'GeneratedModel' when schema has no title#3325matingathani wants to merge 2 commits into
Conversation
json_schema_to_model passes model_name directly to pydantic create_model. When a schema has no 'title' key, model_name is None, causing: TypeError: type.__new__() argument 1 must be str, not None This crashes LangGraph, LangChain, and CrewAI providers whenever they wrap a tool whose parameter schema contains an anonymous nested object (e.g. array items that are objects without an explicit title). Fix: fall back to 'GeneratedModel' when title is absent, matching the convention already used in schema_converter.py for the same purpose. Fixes ComposioHQ#2435
|
@matingathani is attempting to deploy a commit to the Composio Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Pull request overview
Fixes a crash in the Python schema-to-Pydantic conversion path when a JSON schema object lacks a title, which previously caused pydantic.create_model(None, ...) to raise a TypeError. This improves compatibility with providers (LangGraph/LangChain/CrewAI) that encounter anonymous nested object schemas.
Changes:
- Add a fallback model name (
"GeneratedModel") injson_schema_to_modelwhentitleis missing. - Add tests covering title-less schemas (no-crash + required-field behavior).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
python/composio/utils/shared.py |
Prevents create_model from receiving None by adding a fallback model name. |
python/tests/test_schema_parser.py |
Adds regression tests for title-less object schemas and required-field enforcement. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…name in test Address Copilot review feedback on PR ComposioHQ#3325: - Use 'title' in json_schema instead of or-expression to avoid overriding an explicit empty-string title (matches schema_converter.py's pattern) - Assert model_class.__name__ == 'GeneratedModel' in test so the fallback name cannot silently regress
…name in test Address Copilot review feedback on PR ComposioHQ#3325: - Use 'title' in json_schema instead of or-expression to avoid overriding an explicit empty-string title (matches schema_converter.py's pattern) - Assert model_class.__name__ == 'GeneratedModel' in test so the fallback name cannot silently regress
jkomyno
left a comment
There was a problem hiding this comment.
Hi @matingathani — really good catch on this one. I built and locally verified a refinement that I think is worth folding in before merge.
Empirical finding: pydantic.create_model crashes only on None, not on "". I confirmed this by running:
from pydantic import create_model
create_model(None, foo=(str, ...)) # TypeError (the bug)
create_model("", foo=(str, ...)) # OK — produces a model named ''So the original Copilot comment about preserving "" is technically right, but the current key-presence form leaves a real crash uncovered: {"title": None, ...} still hits TypeError: type.__new__() argument 1 must be str, not None. CrewAI / LangChain emit this shape in the wild when older Pydantic v1 paths serialize anonymous models with an explicit-None title.
Two suggestions below: one extends the prod fix to also cover None (preserving "" as Copilot requested); the other parametrizes the test to lock in both crash modes. Both verified locally against the full schema-parser suite (68 tests pass, ruff clean).
| :return: Pydantic `BaseModel` type | ||
| """ | ||
| model_name = json_schema.get("title") | ||
| model_name = json_schema["title"] if "title" in json_schema else "GeneratedModel" |
There was a problem hiding this comment.
The current form correctly handles a missing title key, but {"title": None, ...} still slips through and crashes create_model. Two-line fix that covers both shapes while preserving empty-string titles unchanged (matching Copilot's earlier feedback):
| model_name = json_schema["title"] if "title" in json_schema else "GeneratedModel" | |
| model_name = json_schema.get("title") | |
| if model_name is None: | |
| model_name = "GeneratedModel" |
| def test_no_title_fallback(self): | ||
| """Issue #2435: anonymous schemas (no 'title') must not crash with TypeError. | ||
|
|
||
| LangGraph/LangChain/CrewAI providers call json_schema_to_model for | ||
| array-item schemas that have no 'title' key. Without a fallback, | ||
| create_model(None, ...) raises TypeError: type.__new__() argument 1 | ||
| must be str, not None. | ||
| """ | ||
| json_schema = { | ||
| "type": "object", | ||
| "properties": { | ||
| "name": {"type": "string"}, | ||
| "value": {"type": "integer"}, | ||
| }, | ||
| "required": ["name"], | ||
| } | ||
|
|
||
| model_class = json_schema_to_model(json_schema) | ||
| assert model_class.__name__ == "GeneratedModel" | ||
| instance = model_class(name="test", value=42) | ||
| assert instance.name == "test" | ||
| assert instance.value == 42 |
There was a problem hiding this comment.
If you take the shared.py suggestion above, this test can collapse into one parametrized case that pins both crash modes (missing key + explicit None). Sentinel ... distinguishes "key absent" from "key present with value None":
| def test_no_title_fallback(self): | |
| """Issue #2435: anonymous schemas (no 'title') must not crash with TypeError. | |
| LangGraph/LangChain/CrewAI providers call json_schema_to_model for | |
| array-item schemas that have no 'title' key. Without a fallback, | |
| create_model(None, ...) raises TypeError: type.__new__() argument 1 | |
| must be str, not None. | |
| """ | |
| json_schema = { | |
| "type": "object", | |
| "properties": { | |
| "name": {"type": "string"}, | |
| "value": {"type": "integer"}, | |
| }, | |
| "required": ["name"], | |
| } | |
| model_class = json_schema_to_model(json_schema) | |
| assert model_class.__name__ == "GeneratedModel" | |
| instance = model_class(name="test", value=42) | |
| assert instance.name == "test" | |
| assert instance.value == 42 | |
| @pytest.mark.parametrize( | |
| "title_value", | |
| [ | |
| pytest.param(..., id="missing"), | |
| pytest.param(None, id="none"), | |
| ], | |
| ) | |
| def test_none_title_falls_back_to_generated_model(self, title_value): | |
| """Issue #2435: schemas with missing or None title must not crash. | |
| LangGraph/LangChain/CrewAI providers emit array-item schemas without a | |
| usable title — the key is absent, or present with an explicit None | |
| value. Both forms used to crash create_model with | |
| TypeError: type.__new__() argument 1 must be str, not None. | |
| Empty-string titles are preserved (they don't crash create_model). | |
| """ | |
| json_schema = { | |
| "type": "object", | |
| "properties": { | |
| "name": {"type": "string"}, | |
| "value": {"type": "integer"}, | |
| }, | |
| "required": ["name"], | |
| } | |
| if title_value is not ...: | |
| json_schema["title"] = title_value | |
| model_class = json_schema_to_model(json_schema) | |
| assert model_class.__name__ == "GeneratedModel" | |
| instance = model_class(name="test", value=42) | |
| assert instance.name == "test" | |
| assert instance.value == 42 |
Summary
Fixes a crash in
json_schema_to_modelwhen a JSON schema has notitlefield.Root cause:
json_schema_to_modelcallscreate_model(model_name, ...)wheremodel_name = json_schema.get("title"). Whentitleis absent,model_nameisNone, and Pydantic raises:This hits in practice when LangGraph/LangChain/CrewAI providers wrap tools whose parameter schemas contain anonymous nested objects — for example, array-item schemas that have no explicit
titlekey.Fix: Fall back to
"GeneratedModel"whentitleis absent, matching the convention already used inschema_converter.pyfor the same purpose.Stack trace from the issue:
Test plan
json_schema_to_modelwith notitlecreates a valid model (no crash)pytest tests/test_schema_parser.py— 67 tests)pytest tests/— 592 tests)ruff check)Closes #2435