Repository navigation
Gemma 3 models fail with TypeError when a tool returns a datetime or a Pydantic model (e.g. load_memory) #7667
Description
🔴 Required Information
Describe the Bug:
Gemma and Gemma3Ollama turn every function response into text for Gemma 3 with a bare json.dumps(func_response.response) (src/google/adk/models/gemma_llm.py:289). A tool result is stored as the tool returned it (a non-dict result is wrapped as {"result": value}), so any value the json module cannot encode raises TypeError on the next model call and the run fails.
The built-in load_memory tool always hits this, because it returns a LoadMemoryResponse model. So does any tool whose result holds a datetime, bytes or a Pydantic model. The same call keeps the default ensure_ascii=True, so non-ASCII text in a tool result reaches the model as \uXXXX escapes instead of the text itself. LiteLlm sends the text unescaped (ensure_ascii=False).
Steps to Reproduce:
- Install
google-adk2.12.0 (also reproduced onmainatd3247446) withlitellm. - Run the script under "Minimal Reproduction Code". It needs no network or API key: both adapters rewrite the tool result before calling the model.
Expected Behavior:
The tool result reaches Gemma 3 as JSON text, the way a Gemini request carries the same function response: a datetime as an ISO 8601 string, a Pydantic model as an object without its unset fields, bytes as base64, and non-ASCII text as is.
Observed Behavior:
Gemma: TypeError: Object of type datetime is not JSON serializable
Gemma3Ollama: TypeError: Object of type datetime is not JSON serializable
Invoking tool `get_booking` produced: `{"hotel": "ホテル東京"}`.
Environment Details:
- ADK Library Version (pip show google-adk): 2.12.0,
mainatd3247446 - Desktop OS: Windows 11
- Python Version (python -V): Python 3.11.3
Model Information:
- Are you using LiteLLM: Yes for
Gemma3Ollama, No forGemma - Which model is being used:
gemma3:4bon Ollama 0.40.7 (ollama/gemma3:4b),gemma-3-4b-itin the offline script
🟡 Optional Information
Regression:
No. The conversion has used a bare json.dumps since Gemma support was added.
Logs:
The same failure with a real model: an Agent on Gemma3Ollama(model="ollama/gemma3:4b"), run through Runner.
A function tool whose result holds a datetime:
user: Who is the guest on booking B-17, and when did they check in?
tool call: get_booking({'booking_id': 'B-17'})
tool result: {'booking_id': 'B-17', 'guest': 'Zoë Müller', 'hotel': 'ホテル東京', 'checked_in_at': datetime.datetime(2026, 10, 22, 9, 30)}
File ".../google/adk/models/gemma_llm.py", line 421, in generate_content_async
File ".../google/adk/models/gemma_llm.py", line 67, in _move_function_calls_into_system_instruction
File ".../google/adk/models/gemma_llm.py", line 289, in _convert_content_parts_for_gemma
TypeError: Object of type datetime is not JSON serializable
The built-in load_memory tool, with one remembered session in InMemoryMemoryService:
user: Which city do I live in? Look it up in your memory.
tool call: load_memory({'query': 'Which city do I live in?'})
tool result: {'result': LoadMemoryResponse(memories=[MemoryEntry(...)])}
TypeError: Object of type LoadMemoryResponse is not JSON serializable
Screenshots / Video:
N/A
Additional Context:
Gemma 4 is not affected; it uses native function calling through Gemini or LiteLlm.
Minimal Reproduction Code:
import asyncio
import datetime
from google.adk.models.gemma_llm import Gemma
from google.adk.models.gemma_llm import Gemma3Ollama
from google.adk.models.llm_request import LlmRequest
from google.genai import types
def request_with_tool_result(response):
return LlmRequest(
model="gemma-3-4b-it",
contents=[
types.Content(
role="user",
parts=[types.Part.from_text(text="Where is booking B-17?")],
),
types.Content(
role="user",
parts=[
types.Part.from_function_response(
name="get_booking", response=response
)
],
),
],
config=types.GenerateContentConfig(),
)
async def main():
booking = {
"hotel": "ホテル東京",
"checked_in_at": datetime.datetime(2026, 10, 22, 9, 30),
}
for model in (Gemma(), Gemma3Ollama(model="ollama/gemma3:4b")):
try:
# Both adapters rewrite the tool result into text before any network call.
async for _ in model.generate_content_async(request_with_tool_result(booking)):
pass
except TypeError as e:
print(f"{type(model).__name__}: TypeError: {e}")
# Without the datetime the request goes through, but the tool result text
# Gemma 3 reads is escaped.
request = request_with_tool_result({"hotel": "ホテル東京"})
await Gemma()._preprocess_request(request)
print(request.contents[-1].parts[0].text)
asyncio.run(main())How often has this issue occurred?:
- Always (100%)
Activity
surajksharma07 commentedon Oct 22, 2026
Reproduced this on 2.12.0 with both Gemma and Gemma3Ollama @roydonsequeira, including the load_memory case and the \uXXXX escaping.
Running the payload through pydantic_core.to_jsonable_python(..., exclude_none=True, bytes_mode="base64", fallback=str) and then json.dumps(..., ensure_ascii=False) looks like a good workaround and it’s the same direction as your PR #7668. It worked for datetime, bytes and LoadMemoryResponse on my side.
Could you check that it holds up in your setup too for example with None values and nested models in tool results before we move the PR forward?
Also the CLA check on #7668 is failing so please sign it when you get a chance so the review can start.
roydonsequeira commentedon Oct 22, 2026
Thanks for reproducing it. Both hold up. I compared the text Gemma 3 gets with the function response google-genai builds for the same payload in a Gemini request (
_common.convert_to_dict, thenencode_unserializable_types), and the two are identical for every None and nested-model payload I tried:null, so a tool that returns nothing still renders as{"result": null}, exactly as onmain.exclude_noneonly drops Pydantic model fields that are None, as the Gemini request does.datetimeinside a nested dict comes out as an ISO 8601 string.For example, a
Guestmodel with an unsetemail, anAddresswith an unsetpostcode, a list of addresses and anotesdict holdingNoneand adatetime:I pushed 2132e8a to #7668. It adds a
none_valuescase totest_preprocess_request_with_function_response_converts_values, and it fixes the docstring: it said unset model fields are dropped, but what gets dropped is fields that are None (custom_metadata={}is unset and stays). Nested models were already covered: thepydantic_modelcase goes four levels deep, fromLoadMemoryResponsedown toPart.surajksharma07 commentedon Oct 22, 2026
Ran the same cases against 2132e8a’s approach and they match on my side too @roydonsequeira. None in plain dicts stays null ({"result": null} unchanged), nested model None fields are dropped and your Guest/Address example and LoadMemoryResponse come out identical to what google-genai builds for the Gemini request.
One small thing: None fields inside a dataclass are kept (e.g. {"city": "Rome", "note": null}) which is fine since the Gemini request does the same but the “each level drops its None fields” wording only really holds for Pydantic models.
Set, Decimal and date also serialize cleanly now and unknown types fall back to str() instead of crashing so the change looks good.
The only blocker left is the CLA check on #7668 which is still failing. Once that’s signed the workflows can be approved and the review can move ahead.
roydonsequeira commentedon Oct 22, 2026
Good catch, thanks. Only Pydantic types drop their None fields, models and Pydantic dataclasses alike; a standard-library dataclass keeps them, as the Gemini request does. "Each level" was too broad.
The CLA is signed now (Google Individual CLA with GitHub username
roydonsequeira, 10:00 UTC), but the rescan link no longer starts a newcla/googlerun on #7668: the latest one, at 09:47 UTC, predates the signature. Could someone with access re-run the check?surajksharma07 commentedon Oct 22, 2026
Thanks for signing it @roydonsequeira. The cla/google check matches the commit author email not the GitHub username so please check that git log --format='%an <%ae>' -2 on the branch shows the email you signed with (or add that email to the Google account at cla.developers.google.com).
If it matches pushing a new commit or an empty one (git commit --allow-empty -m "chore: re-trigger CLA" && git push) should start a fresh cla/google run. If the email is different amend the author on both commits and force-push.
Also the PR description still says “unset model fields are dropped”. Could you change it to “None fields of Pydantic models and Pydantic dataclasses” to match the docstring you fixed in 2132e8a?
Main (facd7dd) still has the bare json.dumps at gemma_llm.py:289 so #7668 is still needed. Once the CLA check passes the workflows can be approved and it can go to review.