Repository navigation
Description
🔴 Required Information
Describe the Bug:
ADK's non-Gemini adapters send nullable: true as if it were a JSON Schema validation keyword. JSON Schema ignores that OpenAPI keyword, so a Python tool parameter that accepts None is advertised to Claude/OpenAI/LiteLLM with a schema that rejects null.
This also affects ordinary FunctionTool declarations when GOOGLE_GENAI_USE_ENTERPRISE=1: the Vertex sanitizer rewrites anyOf[..., {"type": "null"}] into nullable: true, and the non-Gemini adapters do not restore the null branch. Handwritten types.Schema(nullable=True) follows the same conversion path.
Steps to Reproduce:
- Use an ADK development checkout with the test dependencies (reproduced against the schema-conversion code unchanged through upstream
9a5117b73). - Run with
GOOGLE_GENAI_USE_ENTERPRISE=1:
from typing import Optional
from google.adk.tools import FunctionTool
from google.adk.models.anthropic_llm import function_declaration_to_tool_param
from jsonschema import Draft202012Validator
def weather(city: Optional[str] = None):
"""Look up the weather."""
declaration = FunctionTool(weather)._get_declaration()
schema = function_declaration_to_tool_param(declaration)["input_schema"]
print(schema["properties"]["city"])
print(Draft202012Validator(schema).is_valid({"city": None}))The private conversion methods are used here to inspect the actual provider payload without credentials or a model request. The same rejection reproduces in the LiteLLM and OpenAI tool adapters.
Expected Behavior:
The provider's JSON Schema accepts both a string and null, matching the Python parameter. Non-null constraints remain effective.
Observed Behavior:
{'type': 'string', 'default': None, 'title': 'City', 'nullable': True}
False
Environment Details:
- ADK Library Version: development checkout, 2.13.0
- Desktop OS: macOS
- Python Version: 3.11
Model Information:
- Are you using LiteLLM: reproduced in LiteLLM, Anthropic, and OpenAI conversion paths
- Which model is being used: no live model request; inspecting generated request schemas
🟡 Optional Information
Additional Context:
I am working on a focused fix and would like to contribute it. Would normalizing nullable into a JSON Schema null alternative in the shared schema traversal be an acceptable approach? I plan to cover nested/union/enum schemas, $defs references and strict nullable objects, and preserve the existing Gemini response-schema path. Adjacent PRs #7626, #7591 and #7285 address schema serialization/validation rather than nullable conversion; I inspected their patches before starting.
How often has this issue occurred?:
Always for the reproduced declarations. No claim of live provider acceptance or model behavior is made here.
Activity
surajksharma07 commentedon Nov 2, 2026
Thanks @sumedh-aerram Reproduced this on 2.13.0 and on current main with GOOGLE_GENAI_USE_ENTERPRISE=1: the Anthropic, LiteLLM and OpenAI schemas all keep nullable: true and reject {"city": None}, same for a handwritten Schema(nullable=True).
As a lighter workaround, turning nullable into a type list after lowercase_schema_types (e.g. "string" -> ["string", "null"], adding None to enums, and anyOf with a null branch only when there’s no type) fixed all of them for me. It kept the other constraints working and the adapter tests stayed green. It also leaves $ref paths where they are, which could keep #7925 smaller.
Could you check if that works on your side? Either way, before #7925 goes for review please try it once against a live provider (Claude on Vertex or OpenAI strict mode) and make sure the $ref-heavy cases still validate.
refactor(models): express nullable as JSON Schema type lists
fix(models): keep untyped nullable schemas in place
sumedh-aerram commentedon Nov 2, 2026
Thanks @surajksharma07, that works and it's simpler. #7925 now uses it:
"null"added to their type.enumgetsNone, andconstbecomes an enum withNone, compared with JSON equality so0andFalsestay distinct.anyOf. Otherwise they are left alone, sinceproperties,minimumand similar keywords already accept null.anyOfwrap is kept only where an applicator could still reject null:$ref,allOf,oneOf,not,if/then/else, oranyOfnext totype.$defsvalidate at their original paths, and there are tests for each case.["object", "null"]as an object, so nullable objects keepadditionalProperties: falseand all properties required.The live provider check is next. I'll post the OpenAI strict-mode and Claude results here before asking for review.
sumedh-aerram commentedon Nov 2, 2026
@surajksharma07 I ran the live check against OpenAI strict mode (
gpt-4o-mini, structured output withstrict: true). I ran it on this PR's head and on unchangedmain, through bothOpenAIResponsesLlmandLiteLlm("openai/...").There were two schemas, and the prompt asked for every nullable field to be null:
types.Schemawith a nullable string and a nullable object;$ref-heavy JSON schema: a nullable field inside a$defsentry,$refs to#/$defs/..., a nullable object, and a nullable string.maincity= null (the model filleddetailswith an object, which is also valid)$refschemacity,details,item.noteall nullcity,detailsnull$refschemacity,details,item.noteall nullOn
main, OpenAI ignoresnullable, so strict mode forces the model to make up values. With this PR, every request was accepted, and every reply validated (jsonschema Draft 2020-12) against the schema ADK sent. The model could return null wherever it was asked to. I didn't test Claude on Vertex, because I don't have Vertex access set up.surajksharma07 commentedon Nov 4, 2026
Thanks @sumedh-aerram, the OpenAI strict-mode results look great and the type-list version of #7925 reads much cleaner. Main and 2.13.0 still have the old behaviour so this is still needed.
Two things worth a quick check before review. tools/_node_tool.py also calls lowercase_schema_types so NodeTool schemas that go to Gemini will now carry ["string", "null"] too. And the LiteLLM path now always emits type lists, so one run with LiteLlm("gemini/...") would confirm LiteLLM’s Gemini conversion handles them.
If you can’t get Vertex access, Claude via the direct Anthropic API or LiteLlm("anthropic/...") would cover the same input_schema path.
Once those check out, could you add the live results to the PR’s E2E section (it still says “Not run”)? Then we can get CI approved and the review going.
fix(models): preserve Gemini nullable schemas
sumedh-aerram commentedon Nov 5, 2026
Thanks @surajksharma07, both checks were worth it. The LiteLLM one found a real problem.
LiteLlm("gemini/..."): litellm 1.96.2's own Gemini transform turned["string", "null"]intoanyOfwithoutnullableand droppedenumon nullable enum fields. Before this PR,nullable: truepassed through, so this was a regression. Fixed in fix(models): preserve nullable values in provider JSON schemas #7925: Gemini and Vertex Gemini routes inLiteLlmkeep thenullableform. A test runs litellm's own Gemini conversion on the actual payload and checks thatnullableand the enum survive.NodeTool-> Gemini: rather than rely on Gemini accepting type arrays inparametersJsonSchema, NodeTool declarations now stay byte-identical tomain. A test compares the serialized JSON. The type-list conversion only happens in the OpenAI and Anthropic adapters.Live Gemini (
gemini-3.5-flash), on this PR's head: all 5 requests were accepted. That coversLiteLlm("gemini/...")structured output and function calling, and aNodeToolthrough nativeGemini, each with a nullable string and a nullable enum. The model returnednullwhere asked, every reply validated, and the enums were kept. I don't have Anthropic credentials, so Claude isn't covered, but the OpenAI strict-mode run exercises the same type-list form. The PR's E2E section now has the Gemini and OpenAI results.