googleapis / googleapis/python-genai
VertexAI schema validation issue with complex Union Types
- Dominant language
- Python
- Stars
- 4k
- Forks
- 1k
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 40
Description
Hi, I found a schema validation issue that only affects Vertex AI API calls when processing MCP tools with complex Union types.
The `Schema.from_json_schema()` method fails to add the required top-level `type` field for complex `anyOf` constructs, causing Vertex AI to reject valid JSON Schema that works fine with the Gemini API.
## Steps to reproduce
1. Use adk / any library to create this setup (adk-python version 1.19.0, google-genai version 1.16.1)
agent.py
```python
from google.adk.agents.llm_agent import Agent
from google.adk.tools.mcp_tool import McpToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
from mcp import StdioServerParameters
google_workspace_tool = McpToolset(
connection_params=StdioConnectionParams(
server_params=StdioServerParameters(
command="uvx",
args=[
"workspace-mcp"
],
),
timeout=30
),
tool_filter=["create_event"]
)
root_agent = Agent(
model='gemini-2.5-flash',
name='root_agent',
description='A helpful assistant for user questions.',
instruction='Answer user questions to the best of your knowledge',
tools=[google_workspace_tool]
)
```
The mcp server in use is workspace-mcp - [link to function `create_event`](https://github.com/taylorwilsdon/google_workspace_mcp/blob/main/gcalendar/calendar_tools.py#L477-L493)
```python
async def create_event(
service,
user_google_email: str,
summary: str,
start_time: str,
end_time: str,
calendar_id: str = "primary",
description: Optional[str] = None,
location: Optional[str] = None,
attendees: Optional[List[str]] = None,
timezone: Optional[str] = None,
attachments: Optional[List[str]] = None,
add_google_meet: bool = False,
reminders: Optional[Union[str, List[Dict[str, Any]]]] = None,
use_default_reminders: bool = True,
transparency: Optional[str] = None,
) -> str:
```
... which emits the following json schema to clients
json schema
```json
{
"name": "create_event",
"description": "Creates a new event.\n\nArgs:\n user_google_email (str): The user's Google email address. Required.\n summary (str): Event title.\n start_time (str): Start time (RFC3339, e.g., \"2023-10-27T10:00:00-07:00\" or \"2023-10-27\" for all-day).\n end_time (str): End time (RFC3339, e.g., \"2023-10-27T11:00:00-07:00\" or \"2023-10-28\" for all-day).\n calendar_id (str): Calendar ID (default: 'primary').\n description (Optional[str]): Event description.\n location (Optional[str]): Event location.\n attendees (Optional[List[str]]): Attendee email addresses.\n timezone (Optional[str]): Timezone (e.g., \"America/New_York\").\n attachments (Optional[List[str]]): List of Google Drive file URLs or IDs to attach to the event.\n add_google_meet (bool): Whether to add a Google Meet video conference to the event. Defaults to False.\n reminders (Optional[Union[str, List[Dict[str, Any]]]]): JSON string or list of reminder objects. Each should have 'method' (\"popup\" or \"email\") and 'minutes' (0-40320). Max 5 reminders. Example: '[{\"method\": \"popup\", \"minutes\": 15}]' or [{\"method\": \"popup\", \"minutes\": 15}]\n use_default_reminders (bool): Whether to use calendar's default reminders. If False, uses custom reminders. Defaults to True.\n transparency (Optional[str]): Event transparency for busy/free status. \"opaque\" shows as Busy (default), \"transparent\" shows as Available/Free. Defaults to None (uses Google Calendar default).\n\nReturns:\n str: Confirmation message of the successful event creation with event link.",
"inputSchema": {
"type": "object",
"properties": {
"user_google_email": {
"type": "string"
},
"summary": {
"type": "string"
},
"start_time": {
"type": "string"
},
"end_time": {
"type": "string"
},
"calendar_id": {
"default": "primary",
"type": "string"
},
"description": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"location": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"attendees": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null
},
"timezone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"attachments": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null
},
"add_google_meet": {
"default": false,
"type": "boolean"
},
"reminders": {
"anyOf": [
{
"type": "string"
},
{
"items": {
"additionalProperties": true,
"type": "object"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null
},
"use_default_reminders": {
"default": true,
"type": "boolean"
},
"transparency": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
}
},
"required": [
"user_google_email",
"summary",
"start_time",
"end_time"
]
},
"outputSchema": {
"type": "object",
"properties": {
"result": {
"type": "string"
}
},
"required": [
"result"
],
"x-fastmcp-wrap-result": true
},
"_meta": {
"_fastmcp": {
"tags": []
}
}
}
```
2. Configure adk backend to call google.genai's Vertex AI client with `GOOGLE_GENAI_USE_VERTEXAI=1`
3. Error below will occur when the agent serves any request
```
google.genai.errors.ClientError: 400 INVALID_ARGUMENT. {'error': {'code': 400, 'message': "Unable to submit request because `create_event` functionDeclaration `parameters.reminders` schema didn't specify the schema type field. Learn more: https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling", 'status': 'INVALID_ARGUMENT'}}
```
4. Configure adk backend to call google.genai's Gemini API client with `GOOGLE_GENAI_USE_VERTEXAI=0` and `GEMINI_API_KEY`
5. The agent can now serve requests without validation errors.
## Impact
- MCP tools with > 2 Union types will fail for VertexAI client backend, while Gemini API will proceed successfully.
- A workaround for now is to use simpler Union types for VertexAI e.g. `Optional[str]` which translate to `Union[T, None]`
## Root Cause Analysis
### Data Flow
1. **MCP Server** → Emits valid JSON Schema with `anyOf` constructs
2. **python-adk** → Calls external `google-genai`'s method `mcp_to_gemini_tool()` conversion function.
3. **google-genai** → `Schema.from_json_schema()` processes schema differently for Vertex AI vs Gemini API
### The MCP Tool Conversion Function
In `google/genai/_mcp_utils.py`, the `mcp_to_gemini_tool()` function:
```python
def mcp_to_gemini_tool(tool: McpTool) -> types.Tool:
"""Translates an MCP tool to a Google GenAI tool."""
return types.Tool(
function_declarations=[{
"name": tool.name,
"description": tool.description,
"parameters": types.Schema.from_json_schema(
json_schema=types.JSONSchema(
**_filter_to_supported_schema(tool.inputSchema)
)
),
}]
)
```
### The Schema Conversion Method
In `google/genai/types.py`, the `Schema.from_json_schema()` method:
```python
@classmethod
def from_json_schema(
cls,
*,
json_schema: JSONSchema,
api_option: Literal['VERTEX_AI', 'GEMINI_API'] = 'GEMINI_API',
raise_error_on_unsupported_field: bool = False,
) -> 'Schema':
"""Converts a JSONSchema object to a Schema object.
Note: Conversion of fields that are not included in the JSONSchema class
are ignored.
Json Schema is now supported natively by both Vertex AI and Gemini API.
Users
are recommended to pass/receive Json Schema directly to/from the API. For
example:
1. the counter part of GenerateContentConfig.response_schema is
GenerateContentConfig.response_json_schema, which accepts [JSON
Schema](https://json-schema.org/)
2. the counter part of FunctionDeclaration.parameters is
FunctionDeclaration.parameters_json_schema, which accepts [JSON
Schema](https://json-schema.org/)
3. the counter part of FunctionDeclaration.response is
FunctionDeclaration.response_json_schema, which accepts [JSON
Schema](https://json-schema.org/)
The JSONSchema is compatible with 2020-12 JSON Schema draft, specified by
OpenAPI 3.1.
Args:
json_schema: JSONSchema object to be converted.
api_option: API option to be used. If set to 'VERTEX_AI', the
JSONSchema will be converted to a Schema object that is compatible
with Vertex AI API. If set to 'GEMINI_API', the JSONSchema will be
converted to a Schema object that is compatible with Gemini API.
Default is 'GEMINI_API'.
raise_error_on_unsupported_field: If set to True, an error will be
raised if the JSONSchema contains any unsupported fields. Default is
False.
Returns:
Schema object that is compatible with the specified API option.
Raises:
ValueError: If the JSONSchema contains any unsupported fields and
raise_error_on_unsupported_field is set to True. Or if the JSONSchema
is not compatible with the specified API option.
"""
```
### The Problem: Missing Top-Level Type Field
In `google/genai/types.py`, the Schema class requires a type field:
```python
type: Optional[Type] = Field(
default=None, description="""Optional. The type of the data."""
)
```
When `Schema.from_json_schema()` encounters complex `anyOf` schemas like:
```json
{
"anyOf": [
{"type": "string"},
{"type": "array", "items": {"type": "object"}},
{"type": "null"}
],
"default": null
}
```
It doesn't add the required top-level `type` field that the Vertex AI API demands.
## Test Evidence
In [`google/genai/tests/types/test_schema_from_json_schema.py`](https://github.com/googleapis/python-genai/blob/97cc7e4eafbee4fa4035e7420170ab6a2c9da7fb/google/genai/tests/types/test_schema_from_json_schema.py#L117-L166), the test `test_nullable_in_union_like_type_conversion()` shows how complex unions are handled:
```python
def test_nullable_in_union_like_type_conversion():
"""Test conversion of JSONSchema.nullable to Schema.nullable"""
json_schema1 = types.JSONSchema(
type=[
types.JSONSchemaType('string'),
types.JSONSchemaType('null'),
types.JSONSchemaType('object'),
types.JSONSchemaType('number'),
types.JSONSchemaType('array'),
types.JSONSchemaType('boolean'),
types.JSONSchemaType('integer'),
],
)
gemini_api_schema1 = types.Schema.from_json_schema(json_schema=json_schema1)
vertex_ai_schema1 = types.Schema.from_json_schema(
json_schema=json_schema1, api_option='VERTEX_AI'
)
expected_schema = types.Schema(
nullable=True,
any_of=[
types.Schema(type='STRING'),
types.Schema(type='OBJECT'),
types.Schema(type='NUMBER'),
types.Schema(type='ARRAY'),
types.Schema(type='BOOLEAN'),
types.Schema(type='INTEGER'),
],
)
assert gemini_api_schema1 == expected_schema
assert vertex_ai_schema1 == expected_schema
```
Notice that the expected schema has `nullable=True` and `any_of=[...]` but **no top-level `type` field**. This works for Gemini API but fails for Vertex AI.
## Expected Fix
The fix should be in `Schema.from_json_schema()` to add an appropriate top-level type for complex union schemas when `api_option='VERTEX_AI'`:
```python
if api_option == 'VERTEX_AI' and is_union_like_type and not schema.type:
# Set appropriate type based on union content
schema.type = Type('OBJECT') # or derive from union types
```
Contributor guide
Assessment
This issue has not been assessed yet.