Skip to content

Commit 11ccd68

Browse files
Use unsafe relaxed escaping in AIJsonUtilities.DefaultOptions. (#5850)
1 parent 19fd307 commit 11ccd68

File tree

3 files changed

+51
-8
lines changed

3 files changed

+51
-8
lines changed

src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs

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

44
using System;
55
using System.Collections.Generic;
6+
using System.ComponentModel;
67
using System.Diagnostics.CodeAnalysis;
8+
using System.Text.Encodings.Web;
79
using System.Text.Json;
810
using System.Text.Json.Nodes;
911
using System.Text.Json.Serialization;
@@ -13,7 +15,26 @@ namespace Microsoft.Extensions.AI;
1315

1416
public static partial class AIJsonUtilities
1517
{
16-
/// <summary>Gets the <see cref="JsonSerializerOptions"/> singleton used as the default in JSON serialization operations.</summary>
18+
/// <summary>
19+
/// Gets the <see cref="JsonSerializerOptions"/> singleton used as the default in JSON serialization operations.
20+
/// </summary>
21+
/// <remarks>
22+
/// <para>For Native AOT or applications disabling <see cref="JsonSerializer.IsReflectionEnabledByDefault"/> this instance includes source generated contracts
23+
/// for all common exchange types contained in the Microsoft.Extensions.AI.Abstractions library.
24+
/// </para>
25+
/// <para>
26+
/// It additionally turns on the following settings:
27+
/// <list type="number">
28+
/// <item>Enables the <see cref="JsonSerializerOptions.WriteIndented"/> property.</item>
29+
/// <item>Enables string based enum serialization as implemented by <see cref="JsonStringEnumConverter"/>.</item>
30+
/// <item>Enables <see cref="JsonIgnoreCondition.WhenWritingNull"/> as the default ignore condition for properties.</item>
31+
/// <item>
32+
/// Enables <see cref="JavaScriptEncoder.UnsafeRelaxedJsonEscaping"/> when escaping JSON strings.
33+
/// Consuming applications must ensure that JSON outputs are adequately escaped before embedding in other document formats, such as HTML and XML.
34+
/// </item>
35+
/// </list>
36+
/// </para>
37+
/// </remarks>
1738
public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
1839

1940
/// <summary>Creates the default <see cref="JsonSerializerOptions"/> to use for serialization-related operations.</summary>
@@ -24,25 +45,31 @@ private static JsonSerializerOptions CreateDefaultOptions()
2445
// If reflection-based serialization is enabled by default, use it, as it's the most permissive in terms of what it can serialize,
2546
// and we want to be flexible in terms of what can be put into the various collections in the object model.
2647
// Otherwise, use the source-generated options to enable trimming and Native AOT.
48+
JsonSerializerOptions options;
2749

2850
if (JsonSerializer.IsReflectionEnabledByDefault)
2951
{
3052
// Keep in sync with the JsonSourceGenerationOptions attribute on JsonContext below.
31-
JsonSerializerOptions options = new(JsonSerializerDefaults.Web)
53+
options = new(JsonSerializerDefaults.Web)
3254
{
3355
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
3456
Converters = { new JsonStringEnumConverter() },
3557
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
58+
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
3659
WriteIndented = true,
3760
};
38-
39-
options.MakeReadOnly();
40-
return options;
4161
}
4262
else
4363
{
44-
return JsonContext.Default.Options;
64+
options = new(JsonContext.Default.Options)
65+
{
66+
// Compile-time encoder setting not yet available
67+
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
68+
};
4569
}
70+
71+
options.MakeReadOnly();
72+
return options;
4673
}
4774

4875
// Keep in sync with CreateDefaultOptions above.
@@ -82,5 +109,6 @@ private static JsonSerializerOptions CreateDefaultOptions()
82109
[JsonSerializable(typeof(Embedding<float>))]
83110
[JsonSerializable(typeof(Embedding<double>))]
84111
[JsonSerializable(typeof(AIContent))]
112+
[EditorBrowsable(EditorBrowsableState.Never)] // Never use JsonContext directly, use DefaultOptions instead.
85113
private sealed partial class JsonContext : JsonSerializerContext;
86114
}

src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ private static JsonElement GetJsonSchemaCore(JsonSerializerOptions options, Sche
212212

213213
return schemaObj is null
214214
? _trueJsonSchema
215-
: JsonSerializer.SerializeToElement(schemaObj, JsonContext.Default.JsonNode);
215+
: JsonSerializer.SerializeToElement(schemaObj, options.GetTypeInfo(typeof(JsonNode)));
216216
}
217217

218218
if (key.Type == typeof(void))
@@ -227,7 +227,7 @@ private static JsonElement GetJsonSchemaCore(JsonSerializerOptions options, Sche
227227
};
228228

229229
JsonNode node = options.GetJsonSchemaAsNode(key.Type, exporterOptions);
230-
return JsonSerializer.SerializeToElement(node, JsonContext.Default.JsonNode);
230+
return JsonSerializer.SerializeToElement(node, DefaultOptions.GetTypeInfo(typeof(JsonNode)));
231231

232232
JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, JsonNode schema)
233233
{

test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.ComponentModel;
66
using System.Linq;
7+
using System.Text.Encodings.Web;
78
using System.Text.Json;
89
using System.Text.Json.Nodes;
910
using System.Text.Json.Serialization;
@@ -33,6 +34,20 @@ public static void DefaultOptions_HasExpectedConfiguration()
3334
// Additional settings
3435
Assert.Equal(JsonIgnoreCondition.WhenWritingNull, options.DefaultIgnoreCondition);
3536
Assert.True(options.WriteIndented);
37+
Assert.Same(JavaScriptEncoder.UnsafeRelaxedJsonEscaping, options.Encoder);
38+
}
39+
40+
[Theory]
41+
[InlineData("<script>alert('XSS')</script>", "<script>alert('XSS')</script>")]
42+
[InlineData("""{"forecast":"sunny", "temperature":"75"}""", """{\"forecast\":\"sunny\", \"temperature\":\"75\"}""")]
43+
[InlineData("""{"message":"Πάντα ῥεῖ."}""", """{\"message\":\"Πάντα ῥεῖ.\"}""")]
44+
[InlineData("""{"message":"七転び八起き"}""", """{\"message\":\"七転び八起き\"}""")]
45+
[InlineData("""☺️🤖🌍𝄞""", """☺️\uD83E\uDD16\uD83C\uDF0D\uD834\uDD1E""")]
46+
public static void DefaultOptions_UsesExpectedEscaping(string input, string expectedJsonString)
47+
{
48+
var options = AIJsonUtilities.DefaultOptions;
49+
string json = JsonSerializer.Serialize(input, options);
50+
Assert.Equal($@"""{expectedJsonString}""", json);
3651
}
3752

3853
[Theory]

0 commit comments

Comments
 (0)