Skip to content

FunctionDeclaration.from_callable drops enum and properties from Optional[...] parameters #3105

Closed

Description

@chrikrah

FunctionDeclaration.from_callable keeps only the type of the inner schema when a parameter is Optional[X]. An Optional[Literal["c", "f"]] parameter becomes a nullable string with no enum. An Optional[Address] parameter becomes a bare nullable object with no properties or required. Address | None keeps them, but Literal["c", "f"] | None loses its enum too.

import json
from typing import Literal, Optional
import pydantic
from google import genai
from google.genai import types

client = genai.Client(api_key="not-a-real-key")  # no request is sent

class Address(pydantic.BaseModel):
  city: str

def unit_plain(unit: Literal["c", "f"]) -> str: ...
def unit_optional(unit: Optional[Literal["c", "f"]] = None) -> str: ...
def addr_optional(addr: Optional[Address] = None) -> str: ...
def addrs_optional(addrs: Optional[list[Address]] = None) -> str: ...
def unit_pipe(unit: Literal["c", "f"] | None = None) -> str: ...
def addr_pipe(addr: Address | None = None) -> str: ...

for f in [unit_plain, unit_optional, addr_optional, addrs_optional, unit_pipe, addr_pipe]:
  fd = types.FunctionDeclaration.from_callable(client=client, callable=f)
  (name, s), = fd.parameters.properties.items()
  print(f"{f.__name__:15}", json.dumps(s.model_dump(exclude_none=True)))
$ python -I genai_fd3.py
unit_plain      {"enum": ["c", "f"], "type": "STRING"}
unit_optional   {"nullable": true, "type": "STRING"}
addr_optional   {"nullable": true, "type": "OBJECT"}
addrs_optional  {"items": {"properties": {"city": {"type": "STRING"}}, "required": ["city"], "type": "OBJECT"}, "nullable": true, "type": "ARRAY"}
unit_pipe       {"nullable": true, "type": "STRING"}
addr_pipe       {"nullable": true, "properties": {"city": {"type": "STRING"}}, "required": ["city"], "type": "OBJECT"}
# google-genai 2.29.0 at 6a6501c (main, 2026-10-11), CPython 3.12.3, Linux x86_64

The single-member collapse in the Union branch of _parse_schema_from_parameter (_automatic_function_calling_util.py:282) copies schema.any_of[0].type and nothing else. Optional[list[...]] survives only because the lines above it copy items by hand. Copying the whole member fixes both:

       if len(schema.any_of) == 1:  # param: Union[List, None] -> Array
-        schema.type = schema.any_of[0].type
-        schema.any_of = None
+        schema = schema.any_of[0].model_copy(
+            update={'nullable': schema.nullable}
+        )
$ python -I genai_fd3.py   # with the patch
unit_plain      {"enum": ["c", "f"], "type": "STRING"}
unit_optional   {"enum": ["c", "f"], "nullable": true, "type": "STRING"}
addr_optional   {"nullable": true, "properties": {"city": {"type": "STRING"}}, "required": ["city"], "type": "OBJECT"}
addrs_optional  {"items": {"properties": {"city": {"type": "STRING"}}, "required": ["city"], "type": "OBJECT"}, "nullable": true, "type": "ARRAY"}
unit_pipe       {"enum": ["c", "f"], "nullable": true, "type": "STRING"}
addr_pipe       {"nullable": true, "properties": {"city": {"type": "STRING"}}, "required": ["city"], "type": "OBJECT"}

$ python -m pytest -q google/genai/tests/types/test_types.py google/genai/tests/types/test_future.py
95 passed, 6 skipped, 2 warnings     # same count before and after the patch
# not run: any request to the live API

This is the default use_json_schema=False path, so it hits direct from_callable callers. A function passed straight into tools=[...] takes use_json_schema=True (_transformers.py:961), where open #3074 and #3093 work. @Venkaiahbabuneelam, CONTRIBUTING.md says outside pull requests are not taken yet, so the diff above is the whole proposal. Would a test for these signatures in tests/types/test_types.py help it land?

Activity

  1. added
    priority: p2Moderately-important priority. Fix may not be included in next release.
    type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.
    and removed
    priority: p2Moderately-important priority. Fix may not be included in next release.
    on Oct 13, 2026
  2. Venkaiahbabuneelam commented on Oct 13, 2026

    @Venkaiahbabuneelam

    Hi @chrikrah,

    Thank you for bringing this to our attention, We have confirmed that types.FunctionDeclaration.from_callable drops enum, properties, and required fields on Optional[...] and Literal[...] | None parameters, We truly appreciate you flagging this issue, we will file a bug internally.
    Thanks

  3. yyyu-google commented on Oct 20, 2026

    @yyyu-google
    Collaborator

    could you set use_json_schema = True? parsing a callable into json schema is the recommended way for the latest API

  4. added 2 commits that reference this issue on Oct 20, 2026
    72da7fa
    9727c40
  5. chrikrah commented on Oct 22, 2026

    @chrikrah
    Author

    @yyyu-google yes, use_json_schema=True works for us and we will switch to it. The three Literal signatures keep their enum, and Address arrives as a $ref with properties and required under $defs.

    $ python genai_fd3.py 1   # report's script, from_callable(..., use_json_schema=True), parameters_json_schema["properties"]
    unit_plain      {"unit": {"enum": ["c", "f"], "type": "string"}}
    unit_optional   {"unit": {"anyOf": [{"enum": ["c", "f"], "type": "string"}, {"type": "null"}], "default": null}}
    addr_optional   {"addr": {"anyOf": [{"$ref": "#/$defs/Address"}, {"type": "null"}], "default": null}}
    addrs_optional  {"addrs": {"anyOf": [{"items": {"$ref": "#/$defs/Address"}, "type": "array"}, {"type": "null"}], "default": null}}
    unit_pipe       {"unit": {"anyOf": [{"enum": ["c", "f"], "type": "string"}, {"type": "null"}], "default": null}}
    addr_pipe       {"addr": {"anyOf": [{"$ref": "#/$defs/Address"}, {"type": "null"}], "default": null}}
    
    $ python genai_fd3.py 0   # same script, default path, parameters.properties
    use_json_schema is not True, which is not recommended. Parsing a callable into JSON schema is preferred.
    unit_plain      {"enum": ["c", "f"], "type": "STRING"}
    unit_optional   {"nullable": true, "type": "STRING"}
    addr_optional   {"nullable": true, "type": "OBJECT"}
    addrs_optional  {"items": {"properties": {"city": {"type": "STRING"}}, "required": ["city"], "type": "OBJECT"}, "nullable": true, "type": "ARRAY"}
    unit_pipe       {"nullable": true, "type": "STRING"}
    addr_pipe       {"nullable": true, "properties": {"city": {"type": "STRING"}}, "required": ["city"], "type": "OBJECT"}
    # google-genai at 9727c40 (head of #3175), CPython 3.12.3, no request sent
    

    At the same head the default path disagrees with itself: a bare Literal keeps its enum and Address | None keeps properties, while Optional[Address] and both nullable Literal spellings lose them. That is the bug confirmed in triage above, and #3175 adds the warning without touching it.

    Would you take the three-line collapse fix from the report alongside #3175, or should #3175 say Refs #3105 so this stays open for it?

  6. added 2 commits that reference this issue on Oct 23, 2026
    34b6257
    6bd3d40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

priority: p2Moderately-important priority. Fix may not be included in next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions