From feec928945eba7b332dc128dc505f634f85c5ad6 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 6 Feb 2025 19:05:29 +0000 Subject: [PATCH 1/2] Use unsafe relaxed escaping in AIJsonUtilities.DefaultOptions. --- .../Utilities/AIJsonUtilities.Defaults.cs | 40 ++++++++++++++++--- .../Utilities/AIJsonUtilities.Schema.cs | 4 +- .../Utilities/AIJsonUtilitiesTests.cs | 15 +++++++ 3 files changed, 51 insertions(+), 8 deletions(-) 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..af517ed5caa 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 HTML documents. + /// + /// + /// + /// 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] From ea71ec8dab7376777e159fd50c7d1a35eac816a2 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 6 Feb 2025 20:46:43 +0000 Subject: [PATCH 2/2] Update src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs --- .../Utilities/AIJsonUtilities.Defaults.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 af517ed5caa..9ae0fac76c4 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -30,7 +30,7 @@ public static partial class AIJsonUtilities /// 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 HTML documents. + /// Consuming applications must ensure that JSON outputs are adequately escaped before embedding in other document formats, such as HTML and XML. /// /// ///