Skip to content

VertexAI schema validation issue with complex Union Types #1807

@luutuankiet

Description

@luutuankiet

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
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

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
{
      "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": []
        }
      }
}
  1. Configure adk backend to call google.genai's Vertex AI client with GOOGLE_GENAI_USE_VERTEXAI=1
  2. 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'}}
  1. Configure adk backend to call google.genai's Gemini API client with GOOGLE_GENAI_USE_VERTEXAI=0 and GEMINI_API_KEY
  2. 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-genaiSchema.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:

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:

@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:

type: Optional[Type] = Field(
    default=None, description="""Optional. The type of the data."""
)

When Schema.from_json_schema() encounters complex anyOf schemas like:

{
  "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, the test test_nullable_in_union_like_type_conversion() shows how complex unions are handled:

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':

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

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