Please excuse the robot-generated issue text. I've isolated and confirmed the bug, and that the monkey patches fix it.
When converting an MCP-style tool dict (or any JSON Schema with nested object properties) to Gemini's tool format, _function_utils._get_properties_from_schema corrupts nested objects in two distinct ways.
Affected versions: langchain-google-genai==4.2.2 (latest); google-genai==1.73.1.
Bug A — required is overwritten with "every property without a default"
_function_utils.py lines 573–576:
if isinstance(v_properties, dict):
properties_item["required"] = [
k for k, v in v_properties.items() if "default" not in v
]
For any nested OBJECT property, the converter discards the source schema's own required array and rebuilds it as "every property that lacks an explicit default key". A schema with required: ["name", "role"] and 6 nullable-optional fields ends up with all 8 fields marked required.
No existing issue or PR I could find addresses this.
Bug B — nullable flag dropped on the second pass
The converter is invoked twice during a single tool conversion: once by _format_json_schema_to_gapic on the outer schema, and again when _dict_to_genai_schema recurses into the inner object. The first pass correctly converts anyOf:[T, {"type":"null"}] → {type: T, nullable: true}. The second pass calls _is_nullable_schema, which only recognizes anyOf shape and the NULL type — not an explicit nullable: true key. So the flag silently disappears on second pass.
PR #1470 fixes this from the other side (avoids producing the explicit nullable: true shape in the first place); my fix below patches _is_nullable_schema to recognize the shape that already gets emitted. Either approach works.
Minimum reproduction
Self-contained, copy-paste runnable. Requires only pip install langchain-google-genai==4.2.2.
from langchain_google_genai._function_utils import _format_to_genai_function_declaration
tool = {
"name": "create_contact",
"description": "Create an administrative contact under a group.",
"parameters": {
"type": "object",
"properties": {
"contact": {
"type": "object",
"properties": {
"name": {"type": "string"},
"role": {"type": "string"},
"email": {"anyOf": [{"type": "string"}, {"type": "null"}]},
"phone": {"anyOf": [{"type": "string"}, {"type": "null"}]},
},
"required": ["name", "role"],
},
},
"required": ["contact"],
},
}
fn = _format_to_genai_function_declaration(tool)
contact = fn.parameters.properties["contact"]
# Bug A: required is overwritten with all properties (not just the input list)
print("required:", contact.required)
assert contact.required == ["name", "role"], \
f"Bug A: expected ['name', 'role'], got {contact.required}"
# Bug B: nullable flag is dropped on nested anyOf:[T, null]
print("email.nullable:", contact.properties["email"].nullable)
assert contact.properties["email"].nullable is True, \
f"Bug B: expected nullable=True, got {contact.properties['email'].nullable}"
Output on 4.2.2:
required: ['name', 'role', 'email', 'phone']
AssertionError: Bug A: expected ['name', 'role'], got ['name', 'role', 'email', 'phone']
(Bug B asserts after Bug A is fixed; the demo shows both fail.)
Proposed fix
Two surgical changes. Running these as monkey patches in production: both bugs disappear, our 9-case regression suite (top-level happy paths + nested + the real create_contact shape) goes 4/9 → 9/9 passing.
Bug A — honor source required when present
In _get_properties_from_schema, replace lines 573–576 with:
if isinstance(v_properties, dict):
properties_item["required"] = (
list(v["required"])
if isinstance(v.get("required"), list)
else []
)
(The discarded synthesis branch was a heuristic for "Optional fields have a default set"; in practice both Pydantic's model_json_schema() and raw JSON Schema specify required explicitly, so a missing required should be honored as "no required fields", not "everything that lacks default". My original proposed fix above kept the synthesis as a fallback, but verifying against the real production case — a nested address object whose source had no required — showed the fallback still produces the bug.)
Bug B — detect explicit nullable: true
In _is_nullable_schema, add at the top:
def _is_nullable_schema(schema: dict[str, Any]) -> bool:
if schema.get("nullable") is True:
return True
# ... existing logic ...
(Or merge with PR #1470's approach, which fixes the other end of the same round-trip.)
Happy to send a PR with both fixes plus the repro converted into a regression test if it'd help — let me know.
When converting an MCP-style tool dict (or any JSON Schema with nested
objectproperties) to Gemini's tool format,_function_utils._get_properties_from_schemacorrupts nested objects in two distinct ways.Affected versions:
langchain-google-genai==4.2.2(latest);google-genai==1.73.1.Bug A —
requiredis overwritten with "every property without a default"_function_utils.pylines 573–576:For any nested OBJECT property, the converter discards the source schema's own
requiredarray and rebuilds it as "every property that lacks an explicitdefaultkey". A schema withrequired: ["name", "role"]and 6 nullable-optional fields ends up with all 8 fields marked required.No existing issue or PR I could find addresses this.
Bug B —
nullableflag dropped on the second passThe converter is invoked twice during a single tool conversion: once by
_format_json_schema_to_gapicon the outer schema, and again when_dict_to_genai_schemarecurses into the inner object. The first pass correctly convertsanyOf:[T, {"type":"null"}]→{type: T, nullable: true}. The second pass calls_is_nullable_schema, which only recognizesanyOfshape and theNULLtype — not an explicitnullable: truekey. So the flag silently disappears on second pass.PR #1470 fixes this from the other side (avoids producing the explicit
nullable: trueshape in the first place); my fix below patches_is_nullable_schemato recognize the shape that already gets emitted. Either approach works.Minimum reproduction
Self-contained, copy-paste runnable. Requires only
pip install langchain-google-genai==4.2.2.Output on
4.2.2:(Bug B asserts after Bug A is fixed; the demo shows both fail.)
Proposed fix
Two surgical changes. Running these as monkey patches in production: both bugs disappear, our 9-case regression suite (top-level happy paths + nested + the real
create_contactshape) goes 4/9 → 9/9 passing.Bug A — honor source
requiredwhen presentIn
_get_properties_from_schema, replace lines 573–576 with:(The discarded synthesis branch was a heuristic for "Optional fields have a
defaultset"; in practice both Pydantic'smodel_json_schema()and raw JSON Schema specifyrequiredexplicitly, so a missingrequiredshould be honored as "no required fields", not "everything that lacksdefault". My original proposed fix above kept the synthesis as a fallback, but verifying against the real production case — a nestedaddressobject whose source had norequired— showed the fallback still produces the bug.)Bug B — detect explicit
nullable: trueIn
_is_nullable_schema, add at the top:(Or merge with PR #1470's approach, which fixes the other end of the same round-trip.)
Happy to send a PR with both fixes plus the repro converted into a regression test if it'd help — let me know.