Gemini: nullable JSON Schema types are serialized into FunctionDeclaration.parameters and rejected

Author: saitewasresetCreated Sep 9, 2026Updated Sep 9, 2026

Summary

The Gemini provider sends generic JSON Schema tool parameters through FunctionDeclaration.parameters.

When a schema contains a nullable JSON Schema type such as:

{
  "type": ["string", "null"]
}

Gemini rejects the request because FunctionDeclaration.parameters uses Gemini's OpenAPI-based Schema, where type is a scalar enum rather than an array.

The request fails before model inference with HTTP 400:

Invalid JSON payload received. Unknown name "type" at
'tools[0].function_declarations[0].parameters.properties[1].value.items.properties[3].value':
Proto field is not repeating, cannot start list.

This is reproducible with LiteLLM v1.8.10 and the current repository commit 1d09e38ef9b756ab710c296350e551f6e178406d.

Origin of the function declaration

The original function is audit_foundation from github.com/voocel/ainovel-cli.

The function declaration is defined here:

https://github.com/voocel/ainovel-cli/blob/fc17855818660da2d729616842fbb475cbc0136e/internal/tools/audit_foundation.go#L22-L48

In particular, issues[].suggestion is declared as nullable:

schema.Property(
    "suggestion",
    llmcontract.Nullable(
        schema.String("推荐修改方向;无需建议时为 null"),
    ),
).Required()

llmcontract.Nullable converts a scalar type into a JSON Schema union:

https://github.com/voocel/ainovel-cli/blob/fc17855818660da2d729616842fbb475cbc0136e/internal/llmcontract/contract.go#L158-L164

Equivalent output:

{
  "suggestion": {
    "description": "推荐修改方向;无需建议时为 null",
    "type": ["string", "null"]
  }
}

AINovel v0.7.9 currently depends on github.com/voocel/litellm v1.8.10.

Minimal reproduction

package main

import (
	"context"
	"log"
	"os"

	"github.com/voocel/litellm"
	"github.com/voocel/litellm/provider/gemini"
)

func main() {
	apiKey := os.Getenv("GEMINI_API_KEY")
	if apiKey == "" {
		log.Fatal("GEMINI_API_KEY is required")
	}

	model := os.Getenv("GEMINI_MODEL")
	if model == "" {
		model = "gemini-3.8-flash"
	}

	tool, err := litellm.NewTool(
		"audit_foundation",
		"Audit persisted foundation data for semantic consistency.",
		map[string]any{
			"type": "object",
			"properties": map[string]any{
				"fingerprint": map[string]any{
					"type": "string",
				},
				"ready": map[string]any{
					"type": "boolean",
				},
				"summary": map[string]any{
					"type": "string",
				},
				"issues": map[string]any{
					"type": "array",
					"items": map[string]any{
						"type": "object",
						"properties": map[string]any{
							"artifact": map[string]any{
								"type": "string",
							},
							"description": map[string]any{
								"type": "string",
							},
							"evidence": map[string]any{
								"type": "string",
							},
							"suggestion": map[string]any{
								"type": []string{"string", "null"},
							},
						},
						"required": []string{
							"artifact",
							"description",
							"evidence",
							"suggestion",
						},
					},
				},
			},
			"required": []string{
				"fingerprint",
				"ready",
				"summary",
				"issues",
			},
		},
	)
	if err != nil {
		log.Fatal(err)
	}
	tool.Strict = litellm.StrictEnabled

	client, err := gemini.NewClient(gemini.Config{
		APIKey: apiKey,
	})
	if err != nil {
		log.Fatal(err)
	}

	_, err = client.Chat(context.Background(), litellm.Request{
		Model: model,
		Messages: []litellm.Message{
			litellm.UserText("Call audit_foundation with arbitrary test values."),
		},
		Tools:      []litellm.Tool{tool},
		ToolChoice: "auto",
		MaxTokens:  litellm.IntPtr(64),
	})
	if err != nil {
		log.Fatal(err)
	}
}

Run with:

export GEMINI_API_KEY='...'
go run .

Actual result:

Invalid JSON payload received. Unknown name "type" at
'tools[0].function_declarations[0].parameters.properties[1].value.items.properties[3].value':
Proto field is not repeating, cannot start list.

The same error was reproduced using the Go LiteLLM Gemini provider without AINovel or AgentCore in the call path.

Root cause

The Gemini wire type currently defines function parameters as:

type functionDeclaration struct {
	Name        string         `json:"name"`
	Description string         `json:"description"`
	Parameters  map[string]any `json:"parameters,omitempty"`
}

convertTools unmarshals litellm.Tool.Parameters into a generic map and assigns it directly to Parameters:

var params map[string]any
if len(t.Parameters) > 0 {
	if err := json.Unmarshal(t.Parameters, &params); err != nil {
		return nil, false, fmt.Errorf(
			"gemini: tool %q parameters must be object schema: %w",
			t.Name,
			err,
		)
	}
}

out.FunctionDeclarations = append(
	out.FunctionDeclarations,
	functionDeclaration{
		Name:        t.Name,
		Description: t.Description,
		Parameters:  params,
	},
)

Relevant implementation:

As a result, JSON Schema's array-valued type is sent through Gemini's legacy OpenAPI Schema representation without conversion.

According to the Google Gemini GenerateContent API reference:

  • Gemini's Schema represents a selected subset of OpenAPI 3.0.
  • Schema.type is an enum, so it accepts one scalar type.
  • Schema.nullable is the field used to indicate that a value may be null.

The same API reference documents two mutually exclusive fields on FunctionDeclaration:

  • parameters: Gemini's OpenAPI-based Schema.
  • parametersJsonSchema: a JSON Schema value.

The Gemini function calling guide also describes function parameters as using a selected subset of OpenAPI Schema.

Therefore this JSON Schema:

{
  "type": ["string", "null"]
}

cannot be forwarded unchanged through FunctionDeclaration.parameters.

When using parameters, the equivalent Gemini representation is:

{
  "type": "string",
  "nullable": true
}

Alternatively, raw JSON Schema should be sent using parametersJsonSchema, subject to the Gemini API's supported JSON Schema features.

Expected behavior

The Gemini provider should handle nullable JSON Schema tool parameters before sending the request.

Possible approaches:

  1. Recursively normalize nullable unions when using parameters:

    { "type": ["string", "null"] }
    

    to:

    { "type": "string", "nullable": true }
    
  2. Send generic litellm.Tool.Parameters through FunctionDeclaration.parametersJsonSchema rather than parameters.

  3. If a schema cannot be represented safely by the selected Gemini field, return a local structured validation error instead of forwarding an invalid payload and relying on an HTTP 400 response.

Suggested regression coverage

Please add a Gemini request serialization test covering a nested nullable property, for example:

{
  "type": "object",
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "suggestion": {
            "type": ["string", "null"]
          }
        },
        "required": ["suggestion"]
      }
    }
  },
  "required": ["items"]
}

The resulting Gemini request should either:

  • serialize the nested field as "type": "string", "nullable": true; or
  • place the unmodified JSON Schema under parametersJsonSchema.

It should not serialize an array-valued type under FunctionDeclaration.parameters.