Adapters.ToGemini
C# · package Toolnexus · SPEC §4 · Adapters.cs
public static List<Dictionary<string, object?>> ToGemini(IEnumerable<ITool> tools)Turns tools into the tools array a Gemini generateContent request expects. Same tools, same
execution — only the schema envelope differs.
Gemini is the odd one out: it groups all tools inside a single wrapper object under
functionDeclarations. So the returned list always has exactly one entry, no matter how many
tools you passed.
When to use it
Section titled “When to use it”When you are driving generateContent yourself — the Google GenAI SDK, a raw HttpClient, or
Vertex AI — and need schema for the request body.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. One tool to Gemini schema
Section titled “1. One tool to Gemini schema”using Toolnexus;
var weather = NativeTool.Of( "get_weather", "Current weather for a city", new Dictionary<string, object?> { ["type"] = "object", ["properties"] = new Dictionary<string, object?> { ["city"] = new Dictionary<string, object?> { ["type"] = "string" } }, ["required"] = new[] { "city" }, }, (IDictionary<string, object?> a) => $"sunny in {a["city"]}");
var schema = Adapters.ToGemini(new ITool[] { weather });
// ONE wrapper entry, always — the tools live inside it.if (schema.Count != 1) throw new Exception($"expected 1 wrapper, got {schema.Count}");
var decls = (List<object?>)schema[0]["functionDeclarations"]!;if (decls.Count != 1) throw new Exception("expected 1 declaration");
var fn = (IDictionary<string, object?>)decls[0]!;if (fn["name"] as string != "get_weather") throw new Exception("name");if (fn["description"] as string != "Current weather for a city") throw new Exception("description");if (!fn.ContainsKey("parameters")) throw new Exception("parameters");
Console.WriteLine($"ok: {fn["name"]}");Note the nesting and the cast: the wrapper’s value is a List<object?>, and each element is an
IDictionary<string, object?>. ITool.InputSchema becomes parameters — same rename as OpenAI,
one nesting level shallower.
2. Feeding it straight into a generateContent body
Section titled “2. Feeding it straight into a generateContent body”The output is plain dictionaries and lists, so System.Text.Json serializes it with no converter.
using System.Text.Json;using Toolnexus;
ITool Mk(string name, string desc) => NativeTool.Of( name, desc, new Dictionary<string, object?> { ["type"] = "object", ["properties"] = new Dictionary<string, object?>() }, (IDictionary<string, object?> a) => name);
var tools = new[] { Mk("search", "Search the docs"), Mk("ping", "Health check") };
var body = new Dictionary<string, object?>{ ["contents"] = new[] { new Dictionary<string, object?> { ["role"] = "user", ["parts"] = new[] { new Dictionary<string, object?> { ["text"] = "search for adapters" } }, }, }, ["tools"] = Adapters.ToGemini(tools),};
// Two tools still means ONE wrapper.var wrappers = (List<Dictionary<string, object?>>)body["tools"]!;if (wrappers.Count != 1) throw new Exception($"expected 1 wrapper, got {wrappers.Count}");
var decls = (List<object?>)wrappers[0]["functionDeclarations"]!;// Order is preserved.var names = decls.Select(d => (string)((IDictionary<string, object?>)d!)["name"]!).ToList();if (names[0] != "search" || names[1] != "ping") throw new Exception($"order: {string.Join(",", names)}");
var json = JsonSerializer.Serialize(body);if (!json.Contains("\"functionDeclarations\"")) throw new Exception("expected functionDeclarations on the wire");
Console.WriteLine($"ok: {string.Join(", ", names)}");3. Round-tripping a functionCall back, and the empty-list trap
Section titled “3. Round-tripping a functionCall back, and the empty-list trap”using System.Text.Json;using Toolnexus;
var weather = NativeTool.Of( "get_weather", "Current weather for a city", new Dictionary<string, object?> { ["type"] = "object", ["properties"] = new Dictionary<string, object?> { ["city"] = new Dictionary<string, object?> { ["type"] = "string" } }, ["required"] = new[] { "city" }, }, (IDictionary<string, object?> a) => $"sunny in {a["city"]}");
var tools = new ITool[] { weather };
// What a model would send back: a part with `args` as a real JSON OBJECT.const string part = """{"functionCall":{"name":"get_weather","args":{"city":"Chennai"}}}""";
var call = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(part)!["functionCall"];var calledName = call.GetProperty("name").GetString()!;// NB: `args` is already the implicit parameter of a top-level program — pick another name.var callArgs = call.GetProperty("args").EnumerateObject() .ToDictionary(p => p.Name, p => (object?)p.Value.ToString());
var called = tools.FirstOrDefault(t => t.Name == calledName) ?? throw new Exception("the advertised name should resolve back to the tool");
var res = await called.ExecuteAsync(callArgs);if (res.IsError || res.Output != "sunny in Chennai") throw new Exception(res.Output);
// The trap: NO tools still yields one wrapper, with an empty declaration list —// unlike ToOpenAI/ToAnthropic, which return an empty list. Omit `tools` yourself// when you have none.var empty = Adapters.ToGemini(Array.Empty<ITool>());if (empty.Count != 1) throw new Exception($"expected 1 wrapper, got {empty.Count}");if (((List<object?>)empty[0]["functionDeclarations"]!).Count != 0) throw new Exception("expected 0 declarations");
Console.WriteLine($"ok: {calledName} -> {res.Output} | empty wrappers: {empty.Count}");| Path | From | Notes |
|---|---|---|
[0] |
— | The single wrapper entry. Always present, even for zero tools. |
[0]["functionDeclarations"] |
— | List<object?> — one element per tool, in order. |
…[i]["name"] |
ITool.Name |
What the model calls back with. |
…[i]["description"] |
ITool.Description |
|
…[i]["parameters"] |
ITool.InputSchema |
Renamed — InputSchema → parameters. |
See also
Section titled “See also”Adapters.ToOpenAI·Adapters.ToAnthropicToolkit.CreateAsync— the toolkit exposes this applied to its own toolsLlmClient.Create— calls the adapter for youITool— the four members the adapter reads