Skip to content

Commit 7dd3c77

Browse files
authored
Merge pull request #7 from GmausDev/feature/json-source-generators
Add System.Text.Json source generators for AOT-compatible serialization
2 parents cbab1dd + 9239adf commit 7dd3c77

6 files changed

Lines changed: 87 additions & 38 deletions

File tree

src/CompactifAI.Client/CompactifAI.Client.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
<!-- Package generation -->
2222
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
2323
<GenerateDocumentationFile>true</GenerateDocumentationFile>
24+
25+
<!-- AOT and trimming support -->
26+
<IsTrimmable>true</IsTrimmable>
27+
<IsAotCompatible>true</IsAotCompatible>
2428
</PropertyGroup>
2529

2630
<ItemGroup>

src/CompactifAI.Client/CompactifAIClient.cs

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System.Net.Http.Headers;
22
using System.Net.Http.Json;
33
using System.Text.Json;
4+
using System.Text.Json.Serialization.Metadata;
45
using CompactifAI.Client.Models;
6+
using CompactifAI.Client.Serialization;
57
using Microsoft.Extensions.Options;
68

79
namespace CompactifAI.Client;
@@ -13,7 +15,6 @@ public class CompactifAIClient : ICompactifAIClient
1315
{
1416
private readonly HttpClient _httpClient;
1517
private readonly CompactifAIOptions _options;
16-
private readonly JsonSerializerOptions _jsonOptions;
1718

1819
/// <summary>
1920
/// Creates a new CompactifAI client.
@@ -24,12 +25,6 @@ public CompactifAIClient(HttpClient httpClient, IOptions<CompactifAIOptions> opt
2425
{
2526
_httpClient = httpClient;
2627
_options = options.Value;
27-
_jsonOptions = new JsonSerializerOptions
28-
{
29-
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
30-
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
31-
};
32-
3328
ConfigureHttpClient();
3429
}
3530

@@ -46,12 +41,6 @@ public CompactifAIClient(string apiKey, string? baseUrl = null)
4641
BaseUrl = baseUrl ?? "https://api.compactif.ai/v1"
4742
};
4843
_httpClient = new HttpClient();
49-
_jsonOptions = new JsonSerializerOptions
50-
{
51-
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
52-
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
53-
};
54-
5544
ConfigureHttpClient();
5645
}
5746

@@ -76,10 +65,10 @@ public async Task<ChatCompletionResponse> CreateChatCompletionAsync(
7665
var response = await _httpClient.PostAsJsonAsync(
7766
"chat/completions",
7867
request,
79-
_jsonOptions,
68+
CompactifAIJsonContext.Default.ChatCompletionRequest,
8069
cancellationToken);
8170

82-
return await HandleResponseAsync<ChatCompletionResponse>(response, cancellationToken);
71+
return await HandleResponseAsync(response, CompactifAIJsonContext.Default.ChatCompletionResponse, cancellationToken);
8372
}
8473

8574
/// <inheritdoc />
@@ -120,10 +109,10 @@ public async Task<CompletionResponse> CreateCompletionAsync(
120109
var response = await _httpClient.PostAsJsonAsync(
121110
"completions",
122111
request,
123-
_jsonOptions,
112+
CompactifAIJsonContext.Default.CompletionRequest,
124113
cancellationToken);
125114

126-
return await HandleResponseAsync<CompletionResponse>(response, cancellationToken);
115+
return await HandleResponseAsync(response, CompactifAIJsonContext.Default.CompletionResponse, cancellationToken);
127116
}
128117

129118
/// <inheritdoc />
@@ -176,7 +165,7 @@ public async Task<TranscriptionResponse> TranscribeAsync(
176165

177166
var response = await _httpClient.PostAsync("audio/transcriptions", content, cancellationToken);
178167

179-
return await HandleResponseAsync<TranscriptionResponse>(response, cancellationToken);
168+
return await HandleResponseAsync(response, CompactifAIJsonContext.Default.TranscriptionResponse, cancellationToken);
180169
}
181170

182171
/// <inheritdoc />
@@ -209,15 +198,15 @@ public async Task<ModelsResponse> ListModelsAsync(CancellationToken cancellation
209198
{
210199
var response = await _httpClient.GetAsync("models", cancellationToken);
211200

212-
return await HandleResponseAsync<ModelsResponse>(response, cancellationToken);
201+
return await HandleResponseAsync(response, CompactifAIJsonContext.Default.ModelsResponse, cancellationToken);
213202
}
214203

215204
/// <inheritdoc />
216205
public async Task<ModelInfo> GetModelAsync(string modelId, CancellationToken cancellationToken = default)
217206
{
218207
var response = await _httpClient.GetAsync($"models/{modelId}", cancellationToken);
219208

220-
return await HandleResponseAsync<ModelInfo>(response, cancellationToken);
209+
return await HandleResponseAsync(response, CompactifAIJsonContext.Default.ModelInfo, cancellationToken);
221210
}
222211

223212
#endregion
@@ -226,6 +215,7 @@ public async Task<ModelInfo> GetModelAsync(string modelId, CancellationToken can
226215

227216
private async Task<T> HandleResponseAsync<T>(
228217
HttpResponseMessage response,
218+
JsonTypeInfo<T> jsonTypeInfo,
229219
CancellationToken cancellationToken)
230220
{
231221
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
@@ -238,7 +228,7 @@ private async Task<T> HandleResponseAsync<T>(
238228
responseBody);
239229
}
240230

241-
var result = JsonSerializer.Deserialize<T>(responseBody, _jsonOptions);
231+
var result = JsonSerializer.Deserialize(responseBody, jsonTypeInfo);
242232

243233
if (result is null)
244234
{

src/CompactifAI.Client/Extensions/ServiceCollectionExtensions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics.CodeAnalysis;
12
using Microsoft.Extensions.DependencyInjection;
23
using Microsoft.Extensions.Options;
34

@@ -49,6 +50,12 @@ public static IServiceCollection AddCompactifAI(
4950
/// <param name="services">The service collection.</param>
5051
/// <param name="configuration">The configuration section to bind from.</param>
5152
/// <returns>The service collection for chaining.</returns>
53+
/// <remarks>
54+
/// This method uses configuration binding which requires reflection and is not AOT-compatible.
55+
/// For AOT scenarios, use the overload that accepts an Action&lt;CompactifAIOptions&gt;.
56+
/// </remarks>
57+
[RequiresUnreferencedCode("Configuration binding uses reflection. Use the Action<CompactifAIOptions> overload for AOT scenarios.")]
58+
[RequiresDynamicCode("Configuration binding uses reflection. Use the Action<CompactifAIOptions> overload for AOT scenarios.")]
5259
public static IServiceCollection AddCompactifAI(
5360
this IServiceCollection services,
5461
Microsoft.Extensions.Configuration.IConfiguration configuration)

src/CompactifAI.Client/Models/ChatModels.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Text.Json;
12
using System.Text.Json.Serialization;
23

34
namespace CompactifAI.Client.Models;
@@ -150,7 +151,7 @@ public class ToolFunction
150151
/// </summary>
151152
[JsonPropertyName("parameters")]
152153
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
153-
public object? Parameters { get; set; }
154+
public JsonElement? Parameters { get; set; }
154155
}
155156

156157
/// <summary>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
using CompactifAI.Client.Models;
4+
5+
namespace CompactifAI.Client.Serialization;
6+
7+
/// <summary>
8+
/// Source-generated JSON serialization context for CompactifAI models.
9+
/// Provides high-performance, AOT-compatible serialization without reflection.
10+
/// </summary>
11+
[JsonSourceGenerationOptions(
12+
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower,
13+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
14+
GenerationMode = JsonSourceGenerationMode.Default)]
15+
// Chat models
16+
[JsonSerializable(typeof(ChatCompletionRequest))]
17+
[JsonSerializable(typeof(ChatCompletionResponse))]
18+
[JsonSerializable(typeof(ChatMessage))]
19+
[JsonSerializable(typeof(ChatChoice))]
20+
[JsonSerializable(typeof(Tool))]
21+
[JsonSerializable(typeof(ToolFunction))]
22+
[JsonSerializable(typeof(Usage))]
23+
// Completion models
24+
[JsonSerializable(typeof(CompletionRequest))]
25+
[JsonSerializable(typeof(CompletionResponse))]
26+
[JsonSerializable(typeof(CompletionChoice))]
27+
// Transcription models
28+
[JsonSerializable(typeof(TranscriptionResponse))]
29+
[JsonSerializable(typeof(TranscriptionSegment))]
30+
// Model info
31+
[JsonSerializable(typeof(ModelsResponse))]
32+
[JsonSerializable(typeof(ModelInfo))]
33+
[JsonSerializable(typeof(ModelCapabilities))]
34+
// Supporting types
35+
[JsonSerializable(typeof(List<ChatMessage>))]
36+
[JsonSerializable(typeof(List<ChatChoice>))]
37+
[JsonSerializable(typeof(List<Tool>))]
38+
[JsonSerializable(typeof(List<CompletionChoice>))]
39+
[JsonSerializable(typeof(List<TranscriptionSegment>))]
40+
[JsonSerializable(typeof(List<ModelInfo>))]
41+
[JsonSerializable(typeof(List<string>))]
42+
[JsonSerializable(typeof(JsonElement))]
43+
public partial class CompactifAIJsonContext : JsonSerializerContext
44+
{
45+
}

tests/CompactifAI.Client.Tests/ModelsTests.cs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
using System.Text.Json;
22
using CompactifAI.Client.Models;
3+
using CompactifAI.Client.Serialization;
34
using Xunit;
45

56
namespace CompactifAI.Client.Tests;
67

78
public class ModelsTests
89
{
9-
private readonly JsonSerializerOptions _jsonOptions = new()
10-
{
11-
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
12-
};
10+
// Use source-generated context for high-performance serialization
11+
private static CompactifAIJsonContext JsonContext => CompactifAIJsonContext.Default;
1312

1413
#region ChatMessage Tests
1514

@@ -56,7 +55,7 @@ public void ChatCompletionRequest_Serializes_WithRequiredFields()
5655
}
5756
};
5857

59-
var json = JsonSerializer.Serialize(request, _jsonOptions);
58+
var json = JsonSerializer.Serialize(request, JsonContext.ChatCompletionRequest);
6059

6160
Assert.Contains("\"model\":\"test-model\"", json);
6261
Assert.Contains("\"messages\":", json);
@@ -73,7 +72,7 @@ public void ChatCompletionRequest_Serializes_OptionalFieldsOmittedWhenNull()
7372
Messages = new List<ChatMessage> { ChatMessage.User("Test") }
7473
};
7574

76-
var json = JsonSerializer.Serialize(request, _jsonOptions);
75+
var json = JsonSerializer.Serialize(request, JsonContext.ChatCompletionRequest);
7776

7877
Assert.DoesNotContain("temperature", json);
7978
Assert.DoesNotContain("max_tokens", json);
@@ -93,7 +92,7 @@ public void ChatCompletionRequest_Serializes_WithAllOptionalFields()
9392
FrequencyPenalty = 0.5
9493
};
9594

96-
var json = JsonSerializer.Serialize(request, _jsonOptions);
95+
var json = JsonSerializer.Serialize(request, JsonContext.ChatCompletionRequest);
9796

9897
Assert.Contains("\"temperature\":0.7", json);
9998
Assert.Contains("\"max_tokens\":100", json);
@@ -132,7 +131,7 @@ public void ChatCompletionResponse_Deserializes_Correctly()
132131
}
133132
""";
134133

135-
var response = JsonSerializer.Deserialize<ChatCompletionResponse>(json, _jsonOptions);
134+
var response = JsonSerializer.Deserialize(json, JsonContext.ChatCompletionResponse);
136135

137136
Assert.NotNull(response);
138137
Assert.Equal("chatcmpl-123", response.Id);
@@ -161,7 +160,7 @@ public void CompletionRequest_Serializes_Correctly()
161160
TopP = 0.95
162161
};
163162

164-
var json = JsonSerializer.Serialize(request, _jsonOptions);
163+
var json = JsonSerializer.Serialize(request, JsonContext.CompletionRequest);
165164

166165
Assert.Contains("\"model\":\"test-model\"", json);
167166
Assert.Contains("\"prompt\":\"Once upon a time\"", json);
@@ -198,7 +197,7 @@ public void CompletionResponse_Deserializes_Correctly()
198197
}
199198
""";
200199

201-
var response = JsonSerializer.Deserialize<CompletionResponse>(json, _jsonOptions);
200+
var response = JsonSerializer.Deserialize(json, JsonContext.CompletionResponse);
202201

203202
Assert.NotNull(response);
204203
Assert.Equal("cmpl-123", response.Id);
@@ -230,7 +229,7 @@ public void TranscriptionResponse_Deserializes_Correctly()
230229
}
231230
""";
232231

233-
var response = JsonSerializer.Deserialize<TranscriptionResponse>(json, _jsonOptions);
232+
var response = JsonSerializer.Deserialize(json, JsonContext.TranscriptionResponse);
234233

235234
Assert.NotNull(response);
236235
Assert.Equal("transcribe", response.Task);
@@ -270,7 +269,7 @@ public void ModelsResponse_Deserializes_Correctly()
270269
}
271270
""";
272271

273-
var response = JsonSerializer.Deserialize<ModelsResponse>(json, _jsonOptions);
272+
var response = JsonSerializer.Deserialize(json, JsonContext.ModelsResponse);
274273

275274
Assert.NotNull(response);
276275
Assert.Equal("list", response.Object);
@@ -309,7 +308,7 @@ public void ModelsResponse_ParametersNumber_HandlesVariousFormats(string paramet
309308
}
310309
""";
311310

312-
var response = JsonSerializer.Deserialize<ModelsResponse>(json, _jsonOptions);
311+
var response = JsonSerializer.Deserialize(json, JsonContext.ModelsResponse);
313312

314313
Assert.NotNull(response);
315314
Assert.Single(response.Data);
@@ -338,7 +337,7 @@ public void ModelsResponse_Deserializes_WithMissingParametersNumber()
338337
}
339338
""";
340339

341-
var response = JsonSerializer.Deserialize<ModelsResponse>(json, _jsonOptions);
340+
var response = JsonSerializer.Deserialize(json, JsonContext.ModelsResponse);
342341

343342
Assert.NotNull(response);
344343
Assert.Single(response.Data);
@@ -352,18 +351,21 @@ public void ModelsResponse_Deserializes_WithMissingParametersNumber()
352351
[Fact]
353352
public void Tool_Serializes_Correctly()
354353
{
354+
// Create JsonElement from anonymous object for parameters
355+
var parametersJson = JsonSerializer.SerializeToElement(new { type = "object", properties = new { location = new { type = "string" } } });
356+
355357
var tool = new Tool
356358
{
357359
Type = "function",
358360
Function = new ToolFunction
359361
{
360362
Name = "get_weather",
361363
Description = "Get the current weather",
362-
Parameters = new { type = "object", properties = new { location = new { type = "string" } } }
364+
Parameters = parametersJson
363365
}
364366
};
365367

366-
var json = JsonSerializer.Serialize(tool, _jsonOptions);
368+
var json = JsonSerializer.Serialize(tool, JsonContext.Tool);
367369

368370
Assert.Contains("\"type\":\"function\"", json);
369371
Assert.Contains("\"name\":\"get_weather\"", json);

0 commit comments

Comments
 (0)