diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs index de2c2a695b6..9ae0fac76c4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -13,7 +15,26 @@ namespace Microsoft.Extensions.AI; public static partial class AIJsonUtilities { - /// Gets the singleton used as the default in JSON serialization operations. + /// + /// Gets the singleton used as the default in JSON serialization operations. + /// + /// + /// For Native AOT or applications disabling this instance includes source generated contracts + /// for all common exchange types contained in the Microsoft.Extensions.AI.Abstractions library. + /// + /// + /// It additionally turns on the following settings: + /// + /// Enables the property. + /// Enables string based enum serialization as implemented by . + /// Enables as the default ignore condition for properties. + /// + /// Enables when escaping JSON strings. + /// Consuming applications must ensure that JSON outputs are adequately escaped before embedding in other document formats, such as HTML and XML. + /// + /// + /// + /// public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions(); /// Creates the default to use for serialization-related operations. @@ -24,25 +45,31 @@ private static JsonSerializerOptions CreateDefaultOptions() // If reflection-based serialization is enabled by default, use it, as it's the most permissive in terms of what it can serialize, // and we want to be flexible in terms of what can be put into the various collections in the object model. // Otherwise, use the source-generated options to enable trimming and Native AOT. + JsonSerializerOptions options; if (JsonSerializer.IsReflectionEnabledByDefault) { // Keep in sync with the JsonSourceGenerationOptions attribute on JsonContext below. - JsonSerializerOptions options = new(JsonSerializerDefaults.Web) + options = new(JsonSerializerDefaults.Web) { TypeInfoResolver = new DefaultJsonTypeInfoResolver(), Converters = { new JsonStringEnumConverter() }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, WriteIndented = true, }; - - options.MakeReadOnly(); - return options; } else { - return JsonContext.Default.Options; + options = new(JsonContext.Default.Options) + { + // Compile-time encoder setting not yet available + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; } + + options.MakeReadOnly(); + return options; } // Keep in sync with CreateDefaultOptions above. @@ -82,5 +109,6 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(Embedding))] [JsonSerializable(typeof(Embedding))] [JsonSerializable(typeof(AIContent))] + [EditorBrowsable(EditorBrowsableState.Never)] // Never use JsonContext directly, use DefaultOptions instead. private sealed partial class JsonContext : JsonSerializerContext; } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs index b67fc1ddd77..1af2e5a8c92 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs @@ -212,7 +212,7 @@ private static JsonElement GetJsonSchemaCore(JsonSerializerOptions options, Sche return schemaObj is null ? _trueJsonSchema - : JsonSerializer.SerializeToElement(schemaObj, JsonContext.Default.JsonNode); + : JsonSerializer.SerializeToElement(schemaObj, options.GetTypeInfo(typeof(JsonNode))); } if (key.Type == typeof(void)) @@ -227,7 +227,7 @@ private static JsonElement GetJsonSchemaCore(JsonSerializerOptions options, Sche }; JsonNode node = options.GetJsonSchemaAsNode(key.Type, exporterOptions); - return JsonSerializer.SerializeToElement(node, JsonContext.Default.JsonNode); + return JsonSerializer.SerializeToElement(node, DefaultOptions.GetTypeInfo(typeof(JsonNode))); JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, JsonNode schema) { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs index e79d2c9034e..6e782273f8b 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs @@ -4,6 +4,7 @@ using System; using System.ComponentModel; using System.Linq; +using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -33,6 +34,20 @@ public static void DefaultOptions_HasExpectedConfiguration() // Additional settings Assert.Equal(JsonIgnoreCondition.WhenWritingNull, options.DefaultIgnoreCondition); Assert.True(options.WriteIndented); + Assert.Same(JavaScriptEncoder.UnsafeRelaxedJsonEscaping, options.Encoder); + } + + [Theory] + [InlineData("", "")] + [InlineData("""{"forecast":"sunny", "temperature":"75"}""", """{\"forecast\":\"sunny\", \"temperature\":\"75\"}""")] + [InlineData("""{"message":"Πάντα ῥεῖ."}""", """{\"message\":\"Πάντα ῥεῖ.\"}""")] + [InlineData("""{"message":"七転び八起き"}""", """{\"message\":\"七転び八起き\"}""")] + [InlineData("""☺️🤖🌍𝄞""", """☺️\uD83E\uDD16\uD83C\uDF0D\uD834\uDD1E""")] + public static void DefaultOptions_UsesExpectedEscaping(string input, string expectedJsonString) + { + var options = AIJsonUtilities.DefaultOptions; + string json = JsonSerializer.Serialize(input, options); + Assert.Equal($@"""{expectedJsonString}""", json); } [Theory]