Skip to content

Commit 73962c6

Browse files
Replace STJ boilerplate in the leaf clients with AIJsonUtilities calls. (#5630)
* Replace STJ boilerplate in the leaf clients with AIJsonUtilities calls. * Address feedback. * Address feedback. * Remove redundant using
1 parent ad3b5d0 commit 73962c6

File tree

9 files changed

+84
-122
lines changed

9 files changed

+84
-122
lines changed

src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Runtime.CompilerServices;
88
using System.Text;
99
using System.Text.Json;
10+
using System.Text.Json.Serialization.Metadata;
1011
using System.Threading;
1112
using System.Threading.Tasks;
1213
using Azure.AI.Inference;
@@ -27,6 +28,9 @@ public sealed class AzureAIInferenceChatClient : IChatClient
2728
/// <summary>The underlying <see cref="ChatCompletionsClient" />.</summary>
2829
private readonly ChatCompletionsClient _chatCompletionsClient;
2930

31+
/// <summary>The <see cref="JsonSerializerOptions"/> use for any serialization activities related to tool call arguments and results.</summary>
32+
private JsonSerializerOptions _toolCallJsonSerializerOptions = AIJsonUtilities.DefaultOptions;
33+
3034
/// <summary>Initializes a new instance of the <see cref="AzureAIInferenceChatClient"/> class for the specified <see cref="ChatCompletionsClient"/>.</summary>
3135
/// <param name="chatCompletionsClient">The underlying client.</param>
3236
/// <param name="modelId">The ID of the model to use. If null, it can be provided per request via <see cref="ChatOptions.ModelId"/>.</param>
@@ -51,7 +55,11 @@ public AzureAIInferenceChatClient(ChatCompletionsClient chatCompletionsClient, s
5155
}
5256

5357
/// <summary>Gets or sets <see cref="JsonSerializerOptions"/> to use for any serialization activities related to tool call arguments and results.</summary>
54-
public JsonSerializerOptions? ToolCallJsonSerializerOptions { get; set; }
58+
public JsonSerializerOptions ToolCallJsonSerializerOptions
59+
{
60+
get => _toolCallJsonSerializerOptions;
61+
set => _toolCallJsonSerializerOptions = Throw.IfNull(value);
62+
}
5563

5664
/// <inheritdoc />
5765
public ChatClientMetadata Metadata { get; }
@@ -304,7 +312,7 @@ private ChatCompletionsOptions ToAzureAIOptions(IList<ChatMessage> chatContents,
304312
// These properties are strongly typed on ChatOptions but not on ChatCompletionsOptions.
305313
if (options.TopK is int topK)
306314
{
307-
result.AdditionalProperties["top_k"] = new BinaryData(JsonSerializer.SerializeToUtf8Bytes(topK, JsonContext.Default.Int32));
315+
result.AdditionalProperties["top_k"] = new BinaryData(JsonSerializer.SerializeToUtf8Bytes(topK, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(int))));
308316
}
309317

310318
if (options.AdditionalProperties is { } props)
@@ -317,7 +325,7 @@ private ChatCompletionsOptions ToAzureAIOptions(IList<ChatMessage> chatContents,
317325
default:
318326
if (prop.Value is not null)
319327
{
320-
byte[] data = JsonSerializer.SerializeToUtf8Bytes(prop.Value, JsonContext.GetTypeInfo(prop.Value.GetType(), ToolCallJsonSerializerOptions));
328+
byte[] data = JsonSerializer.SerializeToUtf8Bytes(prop.Value, ToolCallJsonSerializerOptions.GetTypeInfo(typeof(object)));
321329
result.AdditionalProperties[prop.Key] = new BinaryData(data);
322330
}
323331

@@ -419,7 +427,7 @@ private IEnumerable<ChatRequestMessage> ToAzureAIInferenceChatMessages(IEnumerab
419427
{
420428
try
421429
{
422-
result = JsonSerializer.Serialize(resultContent.Result, JsonContext.GetTypeInfo(typeof(object), ToolCallJsonSerializerOptions));
430+
result = JsonSerializer.Serialize(resultContent.Result, ToolCallJsonSerializerOptions.GetTypeInfo(typeof(object)));
423431
}
424432
catch (NotSupportedException)
425433
{
@@ -449,7 +457,7 @@ private IEnumerable<ChatRequestMessage> ToAzureAIInferenceChatMessages(IEnumerab
449457
callRequest.CallId,
450458
new FunctionCall(
451459
callRequest.Name,
452-
JsonSerializer.Serialize(callRequest.Arguments, JsonContext.GetTypeInfo(typeof(IDictionary<string, object>), ToolCallJsonSerializerOptions)))));
460+
JsonSerializer.Serialize(callRequest.Arguments, ToolCallJsonSerializerOptions.GetTypeInfo(typeof(IDictionary<string, object>))))));
453461
}
454462
}
455463

@@ -490,5 +498,6 @@ private static List<ChatMessageContentItem> GetContentParts(IList<AIContent> con
490498

491499
private static FunctionCallContent ParseCallContentFromJsonString(string json, string callId, string name) =>
492500
FunctionCallContent.CreateFromParsedArguments(json, callId, name,
493-
argumentParser: static json => JsonSerializer.Deserialize(json, JsonContext.Default.IDictionaryStringObject)!);
501+
argumentParser: static json => JsonSerializer.Deserialize(json,
502+
(JsonTypeInfo<IDictionary<string, object>>)AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary<string, object>)))!);
494503
}

src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ private EmbeddingsOptions ToAzureAIOptions(IEnumerable<string> inputs, Embedding
173173
{
174174
if (prop.Value is not null)
175175
{
176-
byte[] data = JsonSerializer.SerializeToUtf8Bytes(prop.Value, JsonContext.GetTypeInfo(prop.Value.GetType(), null));
176+
byte[] data = JsonSerializer.SerializeToUtf8Bytes(prop.Value, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
177177
result.AdditionalProperties[prop.Key] = new BinaryData(data);
178178
}
179179
}
Lines changed: 1 addition & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Collections.Generic;
6-
using System.Diagnostics.CodeAnalysis;
74
using System.Text.Json;
85
using System.Text.Json.Serialization;
9-
using System.Text.Json.Serialization.Metadata;
106

117
namespace Microsoft.Extensions.AI;
128

@@ -16,55 +12,4 @@ namespace Microsoft.Extensions.AI;
1612
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
1713
WriteIndented = true)]
1814
[JsonSerializable(typeof(AzureAIChatToolJson))]
19-
[JsonSerializable(typeof(IDictionary<string, object?>))]
20-
[JsonSerializable(typeof(JsonElement))]
21-
[JsonSerializable(typeof(int))]
22-
[JsonSerializable(typeof(long))]
23-
[JsonSerializable(typeof(float))]
24-
[JsonSerializable(typeof(double))]
25-
[JsonSerializable(typeof(bool))]
26-
[JsonSerializable(typeof(float[]))]
27-
[JsonSerializable(typeof(byte[]))]
28-
[JsonSerializable(typeof(sbyte[]))]
29-
internal sealed partial class JsonContext : JsonSerializerContext
30-
{
31-
/// <summary>Gets the <see cref="JsonSerializerOptions"/> singleton used as the default in JSON serialization operations.</summary>
32-
private static readonly JsonSerializerOptions _defaultToolJsonOptions = CreateDefaultToolJsonOptions();
33-
34-
/// <summary>Gets JSON type information for the specified type.</summary>
35-
/// <remarks>
36-
/// This first tries to get the type information from <paramref name="firstOptions"/>,
37-
/// falling back to <see cref="_defaultToolJsonOptions"/> if it can't.
38-
/// </remarks>
39-
public static JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions? firstOptions) =>
40-
firstOptions?.TryGetTypeInfo(type, out JsonTypeInfo? info) is true ?
41-
info :
42-
_defaultToolJsonOptions.GetTypeInfo(type);
43-
44-
/// <summary>Creates the default <see cref="JsonSerializerOptions"/> to use for serialization-related operations.</summary>
45-
[UnconditionalSuppressMessage("AotAnalysis", "IL3050", Justification = "DefaultJsonTypeInfoResolver is only used when reflection-based serialization is enabled")]
46-
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "DefaultJsonTypeInfoResolver is only used when reflection-based serialization is enabled")]
47-
private static JsonSerializerOptions CreateDefaultToolJsonOptions()
48-
{
49-
// If reflection-based serialization is enabled by default, use it, as it's the most permissive in terms of what it can serialize,
50-
// and we want to be flexible in terms of what can be put into the various collections in the object model.
51-
// Otherwise, use the source-generated options to enable trimming and Native AOT.
52-
53-
if (JsonSerializer.IsReflectionEnabledByDefault)
54-
{
55-
// Keep in sync with the JsonSourceGenerationOptions attribute on JsonContext above.
56-
JsonSerializerOptions options = new(JsonSerializerDefaults.Web)
57-
{
58-
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
59-
Converters = { new JsonStringEnumConverter() },
60-
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
61-
WriteIndented = true,
62-
};
63-
64-
options.MakeReadOnly();
65-
return options;
66-
}
67-
68-
return Default.Options;
69-
}
70-
}
15+
internal sealed partial class JsonContext : JsonSerializerContext;

src/Libraries/Microsoft.Extensions.AI.Ollama/JsonContext.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Collections.Generic;
5-
using System.Text.Json;
64
using System.Text.Json.Serialization;
75

86
namespace Microsoft.Extensions.AI;
@@ -23,6 +21,4 @@ namespace Microsoft.Extensions.AI;
2321
[JsonSerializable(typeof(OllamaToolCall))]
2422
[JsonSerializable(typeof(OllamaEmbeddingRequest))]
2523
[JsonSerializable(typeof(OllamaEmbeddingResponse))]
26-
[JsonSerializable(typeof(IDictionary<string, object?>))]
27-
[JsonSerializable(typeof(JsonElement))]
2824
internal sealed partial class JsonContext : JsonSerializerContext;

src/Libraries/Microsoft.Extensions.AI.Ollama/OllamaChatClient.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ public sealed class OllamaChatClient : IChatClient
3030
/// <summary>The <see cref="HttpClient"/> to use for sending requests.</summary>
3131
private readonly HttpClient _httpClient;
3232

33+
/// <summary>The <see cref="JsonSerializerOptions"/> use for any serialization activities related to tool call arguments and results.</summary>
34+
private JsonSerializerOptions _toolCallJsonSerializerOptions = AIJsonUtilities.DefaultOptions;
35+
3336
/// <summary>Initializes a new instance of the <see cref="OllamaChatClient"/> class.</summary>
3437
/// <param name="endpoint">The endpoint URI where Ollama is hosted.</param>
3538
/// <param name="modelId">
@@ -66,7 +69,11 @@ public OllamaChatClient(Uri endpoint, string? modelId = null, HttpClient? httpCl
6669
public ChatClientMetadata Metadata { get; }
6770

6871
/// <summary>Gets or sets <see cref="JsonSerializerOptions"/> to use for any serialization activities related to tool call arguments and results.</summary>
69-
public JsonSerializerOptions? ToolCallJsonSerializerOptions { get; set; }
72+
public JsonSerializerOptions ToolCallJsonSerializerOptions
73+
{
74+
get => _toolCallJsonSerializerOptions;
75+
set => _toolCallJsonSerializerOptions = Throw.IfNull(value);
76+
}
7077

7178
/// <inheritdoc />
7279
public async Task<ChatCompletion> CompleteAsync(IList<ChatMessage> chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default)
@@ -388,24 +395,22 @@ private IEnumerable<OllamaChatRequestMessage> ToOllamaChatRequestMessages(ChatMe
388395

389396
case FunctionCallContent fcc:
390397
{
391-
JsonSerializerOptions serializerOptions = ToolCallJsonSerializerOptions ?? JsonContext.Default.Options;
392398
yield return new OllamaChatRequestMessage
393399
{
394400
Role = "assistant",
395401
Content = JsonSerializer.Serialize(new OllamaFunctionCallContent
396402
{
397403
CallId = fcc.CallId,
398404
Name = fcc.Name,
399-
Arguments = JsonSerializer.SerializeToElement(fcc.Arguments, serializerOptions.GetTypeInfo(typeof(IDictionary<string, object?>))),
405+
Arguments = JsonSerializer.SerializeToElement(fcc.Arguments, ToolCallJsonSerializerOptions.GetTypeInfo(typeof(IDictionary<string, object?>))),
400406
}, JsonContext.Default.OllamaFunctionCallContent)
401407
};
402408
break;
403409
}
404410

405411
case FunctionResultContent frc:
406412
{
407-
JsonSerializerOptions serializerOptions = ToolCallJsonSerializerOptions ?? JsonContext.Default.Options;
408-
JsonElement jsonResult = JsonSerializer.SerializeToElement(frc.Result, serializerOptions.GetTypeInfo(typeof(object)));
413+
JsonElement jsonResult = JsonSerializer.SerializeToElement(frc.Result, ToolCallJsonSerializerOptions.GetTypeInfo(typeof(object)));
409414
yield return new OllamaChatRequestMessage
410415
{
411416
Role = "tool",

src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs

Lines changed: 15 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Diagnostics.CodeAnalysis;
76
using System.Reflection;
87
using System.Runtime.CompilerServices;
98
using System.Text;
@@ -38,6 +37,9 @@ public sealed partial class OpenAIChatClient : IChatClient
3837
/// <summary>The underlying <see cref="ChatClient" />.</summary>
3938
private readonly ChatClient _chatClient;
4039

40+
/// <summary>The <see cref="JsonSerializerOptions"/> use for any serialization activities related to tool call arguments and results.</summary>
41+
private JsonSerializerOptions _toolCallJsonSerializerOptions = AIJsonUtilities.DefaultOptions;
42+
4143
/// <summary>Initializes a new instance of the <see cref="OpenAIChatClient"/> class for the specified <see cref="OpenAIClient"/>.</summary>
4244
/// <param name="openAIClient">The underlying client.</param>
4345
/// <param name="modelId">The model to use.</param>
@@ -80,7 +82,11 @@ public OpenAIChatClient(ChatClient chatClient)
8082
}
8183

8284
/// <summary>Gets or sets <see cref="JsonSerializerOptions"/> to use for any serialization activities related to tool call arguments and results.</summary>
83-
public JsonSerializerOptions? ToolCallJsonSerializerOptions { get; set; }
85+
public JsonSerializerOptions ToolCallJsonSerializerOptions
86+
{
87+
get => _toolCallJsonSerializerOptions;
88+
set => _toolCallJsonSerializerOptions = Throw.IfNull(value);
89+
}
8490

8591
/// <inheritdoc />
8692
public ChatClientMetadata Metadata { get; }
@@ -593,7 +599,7 @@ private sealed class OpenAIChatToolJson
593599
{
594600
try
595601
{
596-
result = JsonSerializer.Serialize(resultContent.Result, JsonContext.GetTypeInfo(typeof(object), ToolCallJsonSerializerOptions));
602+
result = JsonSerializer.Serialize(resultContent.Result, ToolCallJsonSerializerOptions.GetTypeInfo(typeof(object)));
597603
}
598604
catch (NotSupportedException)
599605
{
@@ -622,7 +628,7 @@ private sealed class OpenAIChatToolJson
622628
callRequest.Name,
623629
new(JsonSerializer.SerializeToUtf8Bytes(
624630
callRequest.Arguments,
625-
JsonContext.GetTypeInfo(typeof(IDictionary<string, object?>), ToolCallJsonSerializerOptions)))));
631+
ToolCallJsonSerializerOptions.GetTypeInfo(typeof(IDictionary<string, object?>))))));
626632
}
627633
}
628634

@@ -668,60 +674,19 @@ private static List<ChatMessageContentPart> GetContentParts(IList<AIContent> con
668674

669675
private static FunctionCallContent ParseCallContentFromJsonString(string json, string callId, string name) =>
670676
FunctionCallContent.CreateFromParsedArguments(json, callId, name,
671-
argumentParser: static json => JsonSerializer.Deserialize(json, JsonContext.Default.IDictionaryStringObject)!);
677+
argumentParser: static json => JsonSerializer.Deserialize(json,
678+
(JsonTypeInfo<IDictionary<string, object>>)AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary<string, object>)))!);
672679

673680
private static FunctionCallContent ParseCallContentFromBinaryData(BinaryData ut8Json, string callId, string name) =>
674681
FunctionCallContent.CreateFromParsedArguments(ut8Json, callId, name,
675-
argumentParser: static json => JsonSerializer.Deserialize(json, JsonContext.Default.IDictionaryStringObject)!);
682+
argumentParser: static json => JsonSerializer.Deserialize(json,
683+
(JsonTypeInfo<IDictionary<string, object>>)AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary<string, object>)))!);
676684

677685
/// <summary>Source-generated JSON type information.</summary>
678686
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
679687
UseStringEnumConverter = true,
680688
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
681689
WriteIndented = true)]
682690
[JsonSerializable(typeof(OpenAIChatToolJson))]
683-
[JsonSerializable(typeof(IDictionary<string, object?>))]
684-
[JsonSerializable(typeof(JsonElement))]
685-
private sealed partial class JsonContext : JsonSerializerContext
686-
{
687-
/// <summary>Gets the <see cref="JsonSerializerOptions"/> singleton used as the default in JSON serialization operations.</summary>
688-
private static readonly JsonSerializerOptions _defaultToolJsonOptions = CreateDefaultToolJsonOptions();
689-
690-
/// <summary>Gets JSON type information for the specified type.</summary>
691-
/// <remarks>
692-
/// This first tries to get the type information from <paramref name="firstOptions"/>,
693-
/// falling back to <see cref="_defaultToolJsonOptions"/> if it can't.
694-
/// </remarks>
695-
public static JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions? firstOptions) =>
696-
firstOptions?.TryGetTypeInfo(type, out JsonTypeInfo? info) is true ?
697-
info :
698-
_defaultToolJsonOptions.GetTypeInfo(type);
699-
700-
/// <summary>Creates the default <see cref="JsonSerializerOptions"/> to use for serialization-related operations.</summary>
701-
[UnconditionalSuppressMessage("AotAnalysis", "IL3050", Justification = "DefaultJsonTypeInfoResolver is only used when reflection-based serialization is enabled")]
702-
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "DefaultJsonTypeInfoResolver is only used when reflection-based serialization is enabled")]
703-
private static JsonSerializerOptions CreateDefaultToolJsonOptions()
704-
{
705-
// If reflection-based serialization is enabled by default, use it, as it's the most permissive in terms of what it can serialize,
706-
// and we want to be flexible in terms of what can be put into the various collections in the object model.
707-
// Otherwise, use the source-generated options to enable trimming and Native AOT.
708-
709-
if (JsonSerializer.IsReflectionEnabledByDefault)
710-
{
711-
// Keep in sync with the JsonSourceGenerationOptions attribute on JsonContext above.
712-
JsonSerializerOptions options = new(JsonSerializerDefaults.Web)
713-
{
714-
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
715-
Converters = { new JsonStringEnumConverter() },
716-
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
717-
WriteIndented = true,
718-
};
719-
720-
options.MakeReadOnly();
721-
return options;
722-
}
723-
724-
return Default.Options;
725-
}
726-
}
691+
private sealed partial class JsonContext : JsonSerializerContext;
727692
}

0 commit comments

Comments
 (0)