Summary
Google.GenAI declares IsAotCompatible=true, but a structured-output request whose response schema contains an object with more than one property throws NotSupportedException before the request leaves the process when the application is published with PublishAot=true.
A single-property schema is unaffected and reaches the network normally. The failure does not reproduce on CoreCLR, so it passes dotnet run and unit tests, and appears only in an AOT-published build.
Environment
Google.GenAI 1.18.0
Microsoft.Extensions.AI 10.9.0
- .NET SDK 10.0.201, runtime .NET 10.0.5
PublishAot=true, -r osx-arm64
The compiler predicts it
ILC flags the exact call site during publish:
ILC : Trim analysis warning IL2026: Google.GenAI.Transformers.ProcessJsonNode(JsonNode,HashSet`1<Object>):
Using member 'System.Text.Json.Nodes.JsonArray.Add<String>(String)' which has
'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code.
Creating JsonValue instances with non-primitive types is not compatible with trimming.
Runtime result
Published binary, dummy API key (the failure is pre-transport, so no valid key is needed to observe it):
--- ONE property ---
RESULT: Google.GenAI.ClientError
API key not valid. Please pass a valid API key. <-- reached the network
--- TWO properties ---
RESULT: NotSupportedException (pre-transport)
JsonTypeInfo metadata for type 'System.String' was not provided by TypeInfoResolver of type
'System.Text.Json.Serialization.Metadata.EmptyJsonTypeInfoResolver'. ...
at System.Text.Json.ThrowHelper.ThrowNotSupportedException_NoMetadataForType(Type, IJsonTypeInfoResolver)
at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type, Boolean, Nullable`1, Boolean, Boolean)
at System.Text.Json.Nodes.JsonNode.ConvertFromValue[T](T, Nullable`1)
at Google.GenAI.Transformers.ProcessJsonNode(JsonNode, HashSet`1)
at Google.GenAI.Models.GenerateContentConfigToMldev(ApiClient, JsonNode, JsonObject, JsonNode)
at Google.GenAI.Models.GenerateContentParametersToMldev(ApiClient, JsonNode, JsonObject, JsonNode)
at Google.GenAI.Models.PrivateGenerateContentAsync(...)
at Google.GenAI.Models.GenerateContentAsync(...)
at Microsoft.Extensions.AI.GoogleGenAIChatClient.GetResponseAsync(...)
The same binary run on CoreCLR (dotnet run, no PublishAot) reaches the network for both cases.
Cause
Transformers.ProcessJsonNode injects a propertyOrdering array into any schema object with more than one property, appending each key via JsonArray.Add(string). That generic overload routes through JsonNode.ConvertFromValue<T>, which uses JsonSerializerOptions.Default. Under PublishAot, reflection-based serialisation is disabled and JsonSerializerOptions.Default resolves to EmptyJsonTypeInfoResolver, which has no metadata for System.String.
Because the injection is conditional on property count, one-property schemas never enter the path.
Repro project
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);MEAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.GenAI" Version="1.18.0" />
<PackageReference Include="Microsoft.Extensions.AI" Version="10.9.0" />
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="10.9.0" />
</ItemGroup>
</Project>
using System.Text.Json;
using Google.GenAI;
using Microsoft.Extensions.AI;
var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY") ?? "dummy-key";
await Probe("ONE property", """{"type":"object","properties":{"a":{"type":"string"}},"required":["a"]}""");
await Probe("TWO properties", """{"type":"object","properties":{"a":{"type":"string"},"b":{"type":"string"}},"required":["a","b"]}""");
async Task Probe(string label, string schemaJson)
{
Console.WriteLine($"--- {label} ---");
try
{
var chat = new Client(apiKey: apiKey).AsIChatClient("gemini-3.1-flash-lite");
using var doc = JsonDocument.Parse(schemaJson);
var options = new ChatOptions { ResponseFormat = ChatResponseFormat.ForJsonSchema(doc.RootElement.Clone()) };
var response = await chat.GetResponseAsync([new ChatMessage(ChatRole.User, "Return any JSON matching the schema.")], options);
Console.WriteLine($" succeeded: {response.Text}");
}
catch (Exception ex) { Console.WriteLine($" {ex.GetType().Name}: {ex.Message}"); }
}
dotnet publish -c Release -r osx-arm64 -p:PublishAot=true
./bin/Release/net10.0/osx-arm64/publish/<app>
Suggested fix
Avoid the generic conversion when building the ordering array — append pre-constructed string JsonValue nodes, or supply options with a resolver that covers System.String.
Secondary observation (separate concern)
A clean AOT publish of this minimal console app emits 138 trim/AOT warnings with TrimmerSingleWarn=false. 135 of them originate in Newtonsoft.Json, which is not used by Google.GenAI itself — it arrives transitively through Google.Apis.Auth 1.69.0 (the OAuth2/credential stack). Only 3 originate in Google assemblies.
To be explicit, since the two are easily conflated: Google.GenAI serialises API payloads with System.Text.Json (a direct dependency, System.Text.Json 10.0.8; the assembly contains no reference to Newtonsoft), which is why the crash above sits in System.Text.Json. Newtonsoft.Json is confined to the auth dependency and is a separate obstacle — it is what prevents a warning-clean AOT publish for any consumer. Happy to file that separately.
Summary
Google.GenAIdeclaresIsAotCompatible=true, but a structured-output request whose response schema contains an object with more than one property throwsNotSupportedExceptionbefore the request leaves the process when the application is published withPublishAot=true.A single-property schema is unaffected and reaches the network normally. The failure does not reproduce on CoreCLR, so it passes
dotnet runand unit tests, and appears only in an AOT-published build.Environment
Google.GenAI1.18.0Microsoft.Extensions.AI10.9.0PublishAot=true,-r osx-arm64The compiler predicts it
ILC flags the exact call site during publish:
Runtime result
Published binary, dummy API key (the failure is pre-transport, so no valid key is needed to observe it):
The same binary run on CoreCLR (
dotnet run, noPublishAot) reaches the network for both cases.Cause
Transformers.ProcessJsonNodeinjects apropertyOrderingarray into any schema object with more than one property, appending each key viaJsonArray.Add(string). That generic overload routes throughJsonNode.ConvertFromValue<T>, which usesJsonSerializerOptions.Default. UnderPublishAot, reflection-based serialisation is disabled andJsonSerializerOptions.Defaultresolves toEmptyJsonTypeInfoResolver, which has no metadata forSystem.String.Because the injection is conditional on property count, one-property schemas never enter the path.
Repro project
Suggested fix
Avoid the generic conversion when building the ordering array — append pre-constructed string
JsonValuenodes, or supply options with a resolver that coversSystem.String.Secondary observation (separate concern)
A clean AOT publish of this minimal console app emits 138 trim/AOT warnings with
TrimmerSingleWarn=false. 135 of them originate inNewtonsoft.Json, which is not used byGoogle.GenAIitself — it arrives transitively throughGoogle.Apis.Auth1.69.0 (the OAuth2/credential stack). Only 3 originate in Google assemblies.To be explicit, since the two are easily conflated:
Google.GenAIserialises API payloads withSystem.Text.Json(a direct dependency,System.Text.Json10.0.8; the assembly contains no reference to Newtonsoft), which is why the crash above sits inSystem.Text.Json.Newtonsoft.Jsonis confined to the auth dependency and is a separate obstacle — it is what prevents a warning-clean AOT publish for any consumer. Happy to file that separately.