Skip to content

NativeAOT and Trimming Guidelines

Gunpal Jain edited this page Nov 15, 2026 · 3 revisions

NativeAOT and Trimming Guidelines

NativeAOT and trimming make applications smaller and faster to start, but they restrict runtime reflection and dynamic code generation. This page explains which parts of the SDK work without changes and where you need to supply your own JsonSerializerContext.

What works without changes

  • SDK request and response types. All built-in types are serialized with System.Text.Json source generation, so no reflection-based serialization is needed for the SDK's own types:
    • TypesSerializerContext (namespace GenerativeAI.Types): the core context used by DefaultSerializerOptions.Options for content generation, chat, embeddings, files, caching, models, batch, Live API setup, auth tokens and more.

    • Separate contexts for newer API areas. Each client puts its context first in its own resolver chain:

      Context Used by
      FileSearchSerializerContext FileSearchStoresClient (File Search stores and documents)
      InteractionsSerializerContext InteractionsClient (Interactions API) and, for shared types, the agent API clients
      WebhooksSerializerContext WebhooksClient
      TuningSerializerContext TuningClient
      AgentApisSerializerContext AgentsClient, VoicesClient, EnvironmentsClient, TriggersClient, CredentialsClient, DecisionsClient

      These are separate contexts rather than partials of TypesSerializerContext because the System.Text.Json source generator shipped for net6.0 to net10.0 and netstandard2.0 fails when [JsonSerializable] attributes are split across partial declarations.

  • Source-generated function tools. Tools defined with [FunctionTool] or [GenerateJsonSchema] (see Function Calling) generate their schemas and call code at compile time.

The repository's tests/AotTest project (<PublishAot>true</PublishAot>) exercises these paths.

What needs your own JsonSerializerContext

Your own types are not known to the SDK's contexts. You must register them when you use:

  • JSON mode (GenerateObjectAsync<T>, UseJsonMode<T>, ToObject<T>), see JSON Mode
  • Reflection-based QuickTool / QuickTools, for parameter and return types

JSON mode

using System.Text.Json.Serialization;
using GenerativeAI;

public class MyData
{
    public string Name { get; set; } = string.Empty;
    public int Age { get; set; }
}

[JsonSerializable(typeof(MyData))]
public partial class MyDataJsonContext : JsonSerializerContext { }

Then register it in one of three ways (see Passing a JsonSerializerContext):

// Option 1: global
DefaultSerializerOptions.CustomJsonTypeResolvers.Add(MyDataJsonContext.Default);

// Option 2: per GenerativeModel instance
generativeModel.GenerateObjectJsonSerializerOptions = MyDataJsonContext.Default.Options;

// Option 3: per request / response
var request = new GenerateContentRequest();
request.UseJsonMode<MyData>(MyDataJsonContext.Default.Options);

QuickTools (reflection-based)

Register every parameter type and return type used by the tool:

using System.Text.Json.Serialization;
using GenerativeAI;
using GenerativeAI.Tools;

public class ToolInput  { public string InputText { get; set; } = string.Empty; }
public class ToolOutput { public int Result { get; set; } }

[JsonSerializable(typeof(ToolInput))]
[JsonSerializable(typeof(ToolOutput))]
public partial class MyToolJsonContext : JsonSerializerContext { }

public static class MyTools
{
    public static ToolOutput MyToolMethod(ToolInput input) => new() { Result = input.InputText.Length };
}

// Option 1: global
DefaultSerializerOptions.CustomJsonTypeResolvers.Add(MyToolJsonContext.Default);

// Option 2: pass the options to the constructor
var quickTool = new QuickTool(MyTools.MyToolMethod, options: MyToolJsonContext.Default.Options);

QuickTools(Delegate[] delegates, JsonSerializerOptions? options = null) accepts the same options for several delegates.

Passing a JsonSerializerContext

  1. Global. Add the context to DefaultSerializerOptions.CustomJsonTypeResolvers at startup. It is then used by JSON mode and QuickTools everywhere in the application.

    using GenerativeAI;
    
    public static void ConfigureJsonContext()
    {
        DefaultSerializerOptions.CustomJsonTypeResolvers.Add(MyDataJsonContext.Default);
    }
  2. GenerativeModel scope. Set GenerateObjectJsonSerializerOptions on a model instance. You can skip this if the context is already registered globally.

    using GenerativeAI;
    
    public async Task<MyData?> UseModelScopedContext(string apiKey, string prompt)
    {
        var googleAi = new GoogleAi(apiKey);
        var model = googleAi.CreateGenerativeModel(GoogleAIModels.Gemini38Flash);
        model.GenerateObjectJsonSerializerOptions = MyDataJsonContext.Default.Options;
    
        return await model.GenerateObjectAsync<MyData>(prompt);
    }
  3. Request / response level. Pass the options to GenerateContentRequest.UseJsonMode<T>(options) and GenerateContentResponse.ToObject<T>(options).

    using GenerativeAI;
    using GenerativeAI.Types;
    
    public async Task UseRequestSpecificContext(GenerativeModel model, string prompt)
    {
        var request = new GenerateContentRequest();
        request.AddText(prompt);
        request.UseJsonMode<MyData>(MyDataJsonContext.Default.Options);
    
        var response = await model.GenerateContentAsync(request);
        var myData = response.ToObject<MyData>(MyDataJsonContext.Default.Options);
    
        Console.WriteLine($"Name: {myData?.Name}, Age: {myData?.Age}");
    }

Microsoft.Extensions.AI

GenerativeAIChatClient and the other MEAI adapters (see Microsoft.Extensions.AI IChatClient) use the same serialization. For tools, prefer [FunctionTool(MeaiFunctionTool = true)] or [GenerateJsonSchema(MeaiFunctionTool = true)] and their generated AsMeaiTools(). If you use new QuickTool(...).AsMeaiTool(), register your types as shown above.

Important considerations

  • Register every type. All types used in JSON mode and QuickTools must be in a JsonSerializerContext; a missing type causes a runtime error in AOT or trimmed builds.
  • Performance. Pre-generated serialization code also improves startup time and throughput.
  • Trimming. The trimmer removes unused code; a JsonSerializerContext keeps the serialization code your types need.
  • Reflection limits. Prefer source generation and explicit type registration over runtime reflection.
  • Check warnings. Publish with dotnet publish -c Release (with PublishAot or PublishTrimmed) and fix any IL2026/IL3050 warnings that point at your own reflection-based code.

Related pages

Clone this wiki locally