Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e101bac
fix: Json Schema Generation, used System.Text.Json JsonSchema generat…
Mar 9, 2025
e289da0
fix: warning suppressions
Mar 9, 2025
315af73
feat: Added FunctionTool attribute, which can be used to convert indi…
Mar 10, 2025
36cf256
feat: Added GoogleFunctionTool optional parameters in GenerateJsonSch…
Mar 10, 2025
904bbf0
Merge branch 'tryAGI:main' into main
gunpal5 Mar 10, 2025
6bfd9b7
Merge remote-tracking branch 'origin/main'
Mar 10, 2025
1ebee3f
Updated README.md
Mar 10, 2025
46a569b
fix: schema generation
Mar 10, 2025
d245705
Merge remote-tracking branch 'origin/main'
Mar 10, 2025
8071875
removed uncessary variable
Mar 10, 2025
234c61e
feat: Added M.E.A.I AIFunction generation
Mar 10, 2025
d8a5f36
Simplify description retrieval in SchemaSubsetHelper.
Mar 10, 2025
4075971
Refactor MeaiFunction.cs for improved readability
Mar 10, 2025
28da355
Refactor MeaiFunction to enhance clarity and functionality.
Mar 10, 2025
8830a3f
Update GoogleFunctionTool type references in generator
Mar 10, 2025
6243888
fix: Native AOT for Method Function Tools
Mar 10, 2025
972b9c5
Merge remote-tracking branch 'origin/main'
Mar 10, 2025
b49d3d2
Add snapshot files for ToolsJsonSerializerContext tests
Mar 10, 2025
e30dd35
Disable unused test method in SnapshotTests
Mar 10, 2025
d896319
Merge remote-tracking branch 'origin/main'
Mar 12, 2025
5de972c
Merge remote-tracking branch 'origin/main'
Mar 12, 2025
8c68191
fix: MeaiFunction ignoring simple values types arguments
Mar 12, 2025
b9e53e0
remove unused codes.
Mar 12, 2025
0d1df7a
Merge remote-tracking branch 'origin/main'
Mar 12, 2025
45eaf37
fix: MeaiFunction Type handling, added more robust codes.
Mar 12, 2025
9ba450e
suppressed IL2026, IL3050 for Reflection based type resolver
Mar 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions CSharpToJsonSchema.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30204.135
# Visual Studio Version 17
VisualStudioVersion = 17.12.35506.116 d17.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E793AF18-4371-4EBD-96FC-195EB1798855}"
ProjectSection(SolutionItems) = preProject
Expand All @@ -25,9 +24,9 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{2D8B78DE-7269-417B-9D0B-8981FA513ACB}"
ProjectSection(SolutionItems) = preProject
.github\workflows\auto-merge.yml = .github\workflows\auto-merge.yml
.github\dependabot.yml = .github\dependabot.yml
.github\workflows\dotnet.yml = .github\workflows\dotnet.yml
.github\workflows\pull-request.yml = .github\workflows\pull-request.yml
.github\dependabot.yml = .github\dependabot.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpToJsonSchema", "src\libs\CSharpToJsonSchema\CSharpToJsonSchema.csproj", "{93367DED-6C55-4267-923A-4412D03376FB}"
Expand Down Expand Up @@ -80,10 +79,14 @@ Global
{6167F915-83EB-42F9-929B-AD4719A55811}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6167F915-83EB-42F9-929B-AD4719A55811}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6167F915-83EB-42F9-929B-AD4719A55811}.Release|Any CPU.Build.0 = Release|Any CPU
{1942E3E5-F151-4C90-BECE-140AAD8C66DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1942E3E5-F151-4C90-BECE-140AAD8C66DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC07C90E-A58C-44B3-82D2-E2EB8F777B92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC07C90E-A58C-44B3-82D2-E2EB8F777B92}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC07C90E-A58C-44B3-82D2-E2EB8F777B92}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC07C90E-A58C-44B3-82D2-E2EB8F777B92}.Release|Any CPU.Build.0 = Release|Any CPU
{1942E3E5-F151-4C90-BECE-140AAD8C66DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1942E3E5-F151-4C90-BECE-140AAD8C66DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1942E3E5-F151-4C90-BECE-140AAD8C66DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1942E3E5-F151-4C90-BECE-140AAD8C66DE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public static InterfaceData PrepareMethodData(

return string.Join(".", commonParts);
}

private static OpenApiSchema ToParameterData(ITypeSymbol typeSymbol, string? name = null,
string? description = null, bool isRequired = true)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public partial class {extensionsClassName}
foreach (var tool in tools)
{{
var call = calls[tool.Name];
lst.Add(new global::CSharpToJsonSchema.MeaiFunction(tool, call));
lst.Add(new global::CSharpToJsonSchema.MeaiFunction(tool, call, global::{@interface.Namespace}.{extensionsClassName}JsonSerializerContext.Default.Options));
}}
return lst;
}}
Expand Down Expand Up @@ -59,7 +59,7 @@ public partial class {extensionsClassName}
foreach (var tool in tools)
{{
var call = calls[tool.Name];
lst.Add(new global::CSharpToJsonSchema.MeaiFunction(tool, call));
lst.Add(new global::CSharpToJsonSchema.MeaiFunction(tool, call, global::{@interface.Namespace}.{extensionsClassName}JsonSerializerContext.Default.Options));
}}
return lst;
}}
Expand Down
64 changes: 62 additions & 2 deletions src/libs/CSharpToJsonSchema/MeaiFunction.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Collections.ObjectModel;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Microsoft.Extensions.AI;

namespace CSharpToJsonSchema;
Expand Down Expand Up @@ -29,6 +31,8 @@ public partial class MeaiFunction : AIFunction
/// Gets the description of the tool.
/// </summary>
public override string Description => _tool.Description;

private JsonSerializerOptions? _options;

/// <summary>
/// Gets additional properties associated with the tool.
Expand All @@ -40,7 +44,7 @@ public partial class MeaiFunction : AIFunction
/// </summary>
/// <param name="tool">The tool associated with this function.</param>
/// <param name="call">The function to execute the tool with input arguments.</param>
public MeaiFunction(Tool tool, Func<string, CancellationToken, Task<string>> call)
public MeaiFunction(Tool tool, Func<string, CancellationToken, Task<string>> call, JsonSerializerOptions? options = null)
{
this._tool = tool;
this._call = call;
Expand All @@ -53,7 +57,28 @@ public MeaiFunction(Tool tool, Func<string, CancellationToken, Task<string>> cal
{
tool.AdditionalProperties.Add("Strict", true);
}

_options = options;
}


#pragma warning disable IL2026, IL3050 // Reflection is used only when enabled
private JsonSerializerOptions InitializeReflectionOptions()
{
if(!JsonSerializer.IsReflectionEnabledByDefault)
throw new InvalidOperationException("JsonSerializer.IsReflectionEnabledByDefault is false, please pass in a JsonSerializerOptions instance.");

_options = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true,
Converters = { new JsonStringEnumConverter() },
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
};
return _options;
}
#pragma warning restore IL2026, IL3050 // Reflection is used only when enabled

/// <summary>
/// Invokes the tool with the given arguments asynchronously.
Expand All @@ -76,7 +101,7 @@ public MeaiFunction(Tool tool, Func<string, CancellationToken, Task<string>> cal
/// </summary>
/// <param name="arguments">The arguments to be converted into a JSON string.</param>
/// <returns>A JSON string representation of the arguments.</returns>
private string GetArgsString(IEnumerable<KeyValuePair<string, object?>> arguments)
protected virtual string GetArgsString(IEnumerable<KeyValuePair<string, object?>> arguments)
{
var jsonObject = new JsonObject();

Expand All @@ -93,6 +118,41 @@ private string GetArgsString(IEnumerable<KeyValuePair<string, object?>> argument
}
else if (args.Value is JsonNode node)
{
jsonObject[args.Key] = node;
}
else if (args.Value is JsonValue val)
{
jsonObject[args.Key] = val;
}
else if( args.Value is JsonObject obj)
{
jsonObject[args.Key] = obj;
}
else if (args.Value is JsonArray arr)
{
jsonObject[args.Key] = arr;
}
else
{
var type = args.Value?.GetType();
if(type.IsPrimitive)
{
jsonObject[args.Key] = JsonValue.Create(args.Value);
}
else
{
if (_options == null)
{
//Fallback to Reflection
//This will break the AOT, Hoping for the best, IChatClient implementation only send JSON classes
//Or Developer is using the code generator
_options = InitializeReflectionOptions();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Added reflection fallback with explicit warning

The code includes a fallback to reflection-based serialization with explicit warning comments about AOT compatibility issues. While this provides good backward compatibility, it may lead to runtime errors in AOT environments.

Verify whether this reflection fallback is actually needed or if all scenarios can be covered by passing proper JsonSerializerOptions with type information. If reflection is truly needed, consider adding a feature flag to disable it in AOT environments.


🌐 Web query:

What are best practices for handling System.Text.Json serialization in AOT compilation environments?

💡 Result:

To handle System.Text.Json serialization effectively in AOT compilation environments, follow these best practices:


1. Use Source Generators

Leverage System.Text.Json source generators to avoid reflection, which isn't AOT-compatible. Source generators precompute serialization logic at compile time, ensuring type metadata isn't trimmed.
Example setup:

[JsonSerializable(typeof(MyModel))]
public partial class MyContext : JsonSerializerContext { }

var options = new JsonSerializerOptions {
    TypeInfoResolver = MyContext.Default
};
var json = JsonSerializer.Serialize(model, options);

This eliminates runtime reflection and supports trimming/Native AOT[1][3][7][10].


2. Define a JsonSerializerContext

Create a context class annotated with [JsonSerializable] for all serializable types:

[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Book))]
[JsonSerializable(typeof(Bookcase))]
public partial class AppJsonContext : JsonSerializerContext { }

Register it globally in ASP.NET Core:

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});

This ensures type information is preserved during trimming[2][13][14].


3. Avoid Reflection-Based APIs

Disable reflection defaults to enforce AOT safety:

<PropertyGroup>
    <JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>

This throws InvalidOperationException if reflection is accidentally used[10].


4. Handle Polymorphism with Attributes

Use [JsonPolymorphic] and [JsonDerivedType] for AOT-safe inheritance:

[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(TextItem), "text")]
[JsonDerivedType(typeof(ImageItem), "image")]
public abstract class ItemModel { }

// Context must include all derived types
[JsonSerializable(typeof(ItemModel))]
[JsonSerializable(typeof(TextItem))]
public partial class ItemContext : JsonSerializerContext { }

This replaces reflection-based type resolution[12][7].


5. Configure Serialization Options via Attributes

Set options at compile time using JsonSourceGenerationOptions:

[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
)]
[JsonSerializable(typeof(WeatherForecast))]
public partial class WeatherContext : JsonSerializerContext { }

This avoids runtime configuration overhead[10][13].


6. Handle Enums as Strings

Use JsonStringEnumConverter<TEnum> for AOT-compatible enum serialization:

[JsonConverter(typeof(JsonStringEnumConverter<MyEnum>))]
public enum MyEnum { Value1, Value2 }

[JsonSerializable(typeof(MyEnum))]
public partial class EnumContext : JsonSerializerContext { }

Avoid the non-generic JsonStringEnumConverter[7][10].


7. Follow Model Constraints

  • Use partial classes: Required for source-generated types[1][9].
  • Parameterless constructors: Mandatory for deserialization[1][9].
  • Limit collections: Stick to List<T>, HashSet<T>, and Dictionary<TKey, TValue>[1].
  • Avoid generics: Not supported by some AOT-focused libraries like Chickensoft Serialization[1].

8. Performance and Size Optimization

  • Metadata mode: Reduces generated code size by omitting serialization logic[8].
  • Trimming: Ensure all runtime-accessed types are explicitly included in contexts[4][9].
  • Migrate from Newtonsoft.Json: Replace reflection-heavy serializers with System.Text.Json[5][9].

Example: AOT-Compatible Serialization

// Model
[JsonSerializable(typeof(Book))]
public partial class BookContext : JsonSerializerContext { }

public record Book(
    [property: JsonPropertyName("title")] string Title,
    [property: JsonPropertyName("author")] string Author
);

// Serialization
var options = new JsonSerializerOptions {
    TypeInfoResolver = BookContext.Default
};
string json = JsonSerializer.Serialize(book, options);

Output includes AOT-safe type discriminators:

{
    "$type": "book",
    "title": "The Book",
    "author": "The Author"
}

By adopting these practices, you ensure compatibility with AOT/trimmed environments while maintaining performance and type safety[1][7][10][13].

Citations:


Action Required: Address Reflection Fallback for AOT Compatibility

The reflection-based fallback in use at src/libs/CSharpToJsonSchema/MeaiFunction.cs (lines 144–150) poses a risk in AOT environments where runtime reflection is unsupported. Given best practices for System.Text.Json in AOT scenarios—such as leveraging source generators and predefining a JsonSerializerContext—relying on reflection can lead to runtime errors. If you must retain this fallback for backward compatibility, please consider one of the following actions:

  • Remove the reflection fallback entirely: Ensure all serialization scenarios use properly configured JsonSerializerOptions (e.g., with source-generated type metadata) to eliminate the risk in AOT environments.
  • Gate the fallback with a feature flag: If reflection-based serialization is necessary in some contexts, add a feature flag (or conditional compilation) to disable it when the application is running in AOT scenarios.

var typeInfo = _options.GetTypeInfo(type);

var str = JsonSerializer.Serialize(args.Value, typeInfo);
jsonObject[args.Key] = JsonNode.Parse(str);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Google_GenerativeAI.Microsoft" Version="2.4.1" />
<PackageReference Include="H.Generators.Tests.Extensions" Version="1.24.2" />
<PackageReference Include="H.Resources.Generator" Version="1.8.0">
<PrivateAssets>all</PrivateAssets>
Expand Down
8 changes: 4 additions & 4 deletions src/tests/CSharpToJsonSchema.MeaiTests/Meai_Tests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ClientModel;
using CSharpToJsonSchema.MeaiTests.Services;
using GenerativeAI.Microsoft;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Models;
Expand Down Expand Up @@ -34,7 +35,6 @@ public async Task ShouldInvokeTheFunctions()
.Be(true);

Console.WriteLine(response.Text);

}

//[TestMethod]
Expand All @@ -47,9 +47,10 @@ public async Task ShouldInvokeTheBookService()

var client = new OpenAIClient(new ApiKeyCredential(key));

Microsoft.Extensions.AI.OpenAIChatClient openAiClient = new OpenAIChatClient(client.GetChatClient("gpt-4o-mini"));
//Microsoft.Extensions.AI.OpenAIChatClient openAiClient = new OpenAIChatClient(client.GetChatClient("gpt-4o-mini"));

var chatClient = new Microsoft.Extensions.AI.FunctionInvokingChatClient(openAiClient);
var chatClient = new GenerativeAIChatClient(Environment.GetEnvironmentVariable("GOOGLE_API_KEY",EnvironmentVariableTarget.User));
//var chatClient = new Microsoft.Extensions.AI.FunctionInvokingChatClient(openAiClient);
var chatOptions = new ChatOptions();

var service = new BookStoreService();
Expand All @@ -63,6 +64,5 @@ public async Task ShouldInvokeTheBookService()
.Be(true);

Console.WriteLine(response.Text);

}
}
10 changes: 5 additions & 5 deletions src/tests/CSharpToJsonSchema.SnapshotTests/SnapshotTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ namespace CSharpToJsonSchema.SnapshotTests;
[TestClass]
public class ToolTests : VerifyBase
{
// [TestMethod]
// public Task MethodFunction()
// {
// return this.CheckSourceAsync(H.Resources.MethodFunctionTools_cs.AsString());
// }
[TestMethod]
public Task MethodFunction()
{
return this.CheckSourceAsync(H.Resources.MethodFunctionTools_cs.AsString());
}

[TestMethod]
public Task Weather()
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading