Skip to content

Commit 6f29ab5

Browse files
Make hashing stable w.r.t. indentation settings and property ordering. (#6476)
* Make hashing stable w.r.t. indentation settings and property ordering. * Add indentation invariance test
1 parent 85678c0 commit 6f29ab5

File tree

3 files changed

+51
-3
lines changed

3 files changed

+51
-3
lines changed

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

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
using System.Diagnostics;
77
#endif
88
using System.IO;
9+
using System.Linq;
910
using System.Security.Cryptography;
1011
using System.Text.Json;
12+
using System.Text.Json.Nodes;
1113
using System.Text.Json.Serialization.Metadata;
1214
#if NET
1315
using System.Threading;
@@ -112,7 +114,9 @@ public static string HashDataToString(ReadOnlySpan<object?> values, JsonSerializ
112114
{
113115
foreach (object? value in values)
114116
{
115-
JsonSerializer.Serialize(stream, value, jti);
117+
JsonNode? jsonNode = JsonSerializer.SerializeToNode(value, jti);
118+
NormalizeJsonNode(jsonNode);
119+
JsonSerializer.Serialize(stream, jsonNode, JsonContextNoIndentation.Default.JsonNode!);
116120
}
117121

118122
stream.GetHashAndReset(hashData);
@@ -130,7 +134,9 @@ public static string HashDataToString(ReadOnlySpan<object?> values, JsonSerializ
130134
MemoryStream stream = new();
131135
foreach (object? value in values)
132136
{
133-
JsonSerializer.Serialize(stream, value, jti);
137+
JsonNode? jsonNode = JsonSerializer.SerializeToNode(value, jti);
138+
NormalizeJsonNode(jsonNode);
139+
JsonSerializer.Serialize(stream, jsonNode, JsonContextNoIndentation.Default.JsonNode!);
134140
}
135141

136142
using var hashAlgorithm = SHA384.Create();
@@ -156,6 +162,31 @@ static string ConvertToHexString(ReadOnlySpan<byte> hashData)
156162
return new string(chars);
157163
}
158164
#endif
165+
static void NormalizeJsonNode(JsonNode? node)
166+
{
167+
switch (node)
168+
{
169+
case JsonArray array:
170+
foreach (JsonNode? item in array)
171+
{
172+
NormalizeJsonNode(item);
173+
}
174+
175+
break;
176+
177+
case JsonObject obj:
178+
var entries = obj.OrderBy(e => e.Key, StringComparer.Ordinal).ToArray();
179+
obj.Clear();
180+
181+
foreach (var entry in entries)
182+
{
183+
obj.Add(entry.Key, entry.Value);
184+
NormalizeJsonNode(entry.Value);
185+
}
186+
187+
break;
188+
}
189+
}
159190
}
160191

161192
private static void AddAIContentTypeCore(JsonSerializerOptions options, Type contentType, string typeDiscriminatorId)

src/Libraries/Microsoft.Extensions.AI/ChatCompletion/DistributedCachingChatClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ protected override async Task WriteCacheStreamingAsync(string key, IReadOnlyList
123123
protected override string GetCacheKey(IEnumerable<ChatMessage> messages, ChatOptions? options, params ReadOnlySpan<object?> additionalValues)
124124
{
125125
// Bump the cache version to invalidate existing caches if the serialization format changes in a breaking way.
126-
const int CacheVersion = 1;
126+
const int CacheVersion = 2;
127127

128128
return AIJsonUtilities.HashDataToString([CacheVersion, messages, options, .. additionalValues], _jsonSerializerOptions);
129129
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,13 +557,30 @@ public static void HashData_Idempotent()
557557
string key2 = AIJsonUtilities.HashDataToString(["a", 'b', 42], options);
558558
string key3 = AIJsonUtilities.HashDataToString([TimeSpan.FromSeconds(1), null, 1.23], options);
559559
string key4 = AIJsonUtilities.HashDataToString([TimeSpan.FromSeconds(1), null, 1.23], options);
560+
string key5 = AIJsonUtilities.HashDataToString([new Dictionary<string, object> { ["key1"] = 1, ["key2"] = 2 }], options);
561+
string key6 = AIJsonUtilities.HashDataToString([new Dictionary<string, object> { ["key2"] = 2, ["key1"] = 1 }], options);
560562

561563
Assert.Equal(key1, key2);
562564
Assert.Equal(key3, key4);
565+
Assert.Equal(key5, key6);
563566
Assert.NotEqual(key1, key3);
567+
Assert.NotEqual(key1, key5);
564568
}
565569
}
566570

571+
[Fact]
572+
public static void HashData_IndentationInvariant()
573+
{
574+
JsonSerializerOptions indentOptions = new(AIJsonUtilities.DefaultOptions) { WriteIndented = true };
575+
JsonSerializerOptions noIndentOptions = new(AIJsonUtilities.DefaultOptions) { WriteIndented = false };
576+
577+
Dictionary<string, object> dict = new() { ["key1"] = 1, ["key2"] = 2 };
578+
string key1 = AIJsonUtilities.HashDataToString([dict], indentOptions);
579+
string key2 = AIJsonUtilities.HashDataToString([dict], noIndentOptions);
580+
581+
Assert.Equal(key1, key2);
582+
}
583+
567584
[Fact]
568585
public static void CreateFunctionJsonSchema_InvokesIncludeParameterCallbackForEveryParameter()
569586
{

0 commit comments

Comments
 (0)