From 8027d151f0d140f66064fcf111944cd61896b808 Mon Sep 17 00:00:00 2001 From: Microsoft Graph DevX Tooling Date: Wed, 26 Feb 2025 16:24:42 +0300 Subject: [PATCH 1/5] Added check to ensure that orginal types on values are maintained on values --- tools/Custom/JsonExtensions.cs | 12 ++-- tools/Custom/PreserveStringConverter.cs | 31 ++++++++++ .../JsonUtilitiesTest/JsonExtensionsTests.cs | 56 +++++++++++++++++++ .../JsonUtilitiesTest.csproj | 4 ++ 4 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 tools/Custom/PreserveStringConverter.cs diff --git a/tools/Custom/JsonExtensions.cs b/tools/Custom/JsonExtensions.cs index 08aa3628a10..559bbc75c70 100644 --- a/tools/Custom/JsonExtensions.cs +++ b/tools/Custom/JsonExtensions.cs @@ -131,7 +131,7 @@ public static string ReplaceAndRemoveSlashes(this string body) ProcessBody(jsonToken); // Return cleaned JSON string - return JsonConvert.SerializeObject(jsonToken, Formatting.None); + return JsonConvert.SerializeObject(jsonToken, Formatting.None, new PreserveStringConverter()); } catch (Newtonsoft.Json.JsonException) { @@ -155,8 +155,9 @@ private static void ProcessBody(JToken token) try { JToken parsedValue = JToken.Parse(stringValue); - property.Value = parsedValue; // Replace with unescaped JSON object - ProcessBody(parsedValue); // Recursively process + string originalToken = JsonConvert.SerializeObject(parsedValue, Formatting.None, new PreserveStringConverter()); // Ensures that the value matches the original type + property.Value = originalToken; // Replace with unescaped JSON object + ProcessBody(originalToken); // Recursively process } catch (Newtonsoft.Json.JsonException) { @@ -182,8 +183,9 @@ private static void ProcessBody(JToken token) try { JToken parsedValue = JToken.Parse(stringValue); - jsonArray[i] = parsedValue; // Replace with unescaped JSON object - ProcessBody(parsedValue); // Recursively process + string originalToken = JsonConvert.SerializeObject(parsedValue, Formatting.None, new PreserveStringConverter()); // Ensures that the value matches the original type + jsonArray[i] = originalToken; // Replace with unescaped JSON object + ProcessBody(originalToken); // Recursively process } catch (Newtonsoft.Json.JsonException) { diff --git a/tools/Custom/PreserveStringConverter.cs b/tools/Custom/PreserveStringConverter.cs new file mode 100644 index 00000000000..ac4b0f2a37a --- /dev/null +++ b/tools/Custom/PreserveStringConverter.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace NamespacePrefixPlaceholder.PowerShell.JsonUtilities +{ + public class PreserveStringConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(string); // Only applies to string properties + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + // If the value is a number but the property expects a string, return it as a string + if ((reader.TokenType == JsonToken.Integer || reader.TokenType == JsonToken.Float) && objectType == typeof(string)) + { + return reader.Value?.ToString(); + } + + return reader?.Value; // Otherwise, keep it as is + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value); // Preserve the original type + } + } +} diff --git a/tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs b/tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs index 1ba339600e9..a55e634ac8d 100644 --- a/tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs +++ b/tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs @@ -246,5 +246,61 @@ public void RemoveDefaultNullProperties_ShouldRemoveDefaultNullValuesInJsonObjec Assert.False(result["body"]?["users"][0]?.ToObject().ContainsKey("email")); Assert.True(result["body"]?["users"][0]?["metadata"]?.ToObject().ContainsKey("phone")); } + + [Fact] + public void NumericString_RemainsString() + { + // Arrange + JObject json = JObject.Parse(@"{ + ""displayname"": ""Tim"", + ""position"": ""123"", + ""salary"": 2000000 + }"); + + // Act + string cleanedJson = json.ToString()?.ReplaceAndRemoveSlashes(); + JObject result = JObject.Parse(cleanedJson); + + // Assert + Assert.Equal("123", result["position"]?.ToString()); + Assert.Equal(2000000, result["salary"]?.ToObject()); + } + [Fact] + public void NumericString_RemainsStringInJsonArray() + { + // Arrange + JArray json = JArray.Parse(@"[ + { ""displayname"": ""Tim"", ""position"": ""123"" } + + ]"); + + // Act + string cleanedJson = json.ToString()?.ReplaceAndRemoveSlashes(); + JArray result = JArray.Parse(cleanedJson); + + // Assert + Assert.Equal("123", result[0]?["position"]?.ToString()); + } + + [Fact] + public void NumericString_RemainsStringInNestedJsonObject() + { + // Arrange + JObject json = JObject.Parse(@"{ + ""body"":{ + ""users"": [ + { ""displayname"": ""Tim"", ""position"": ""123"" } + ] + } + }"); + + // Act + string cleanedJson = json.ToString()?.ReplaceAndRemoveSlashes(); + JObject result = JObject.Parse(cleanedJson); + + // Assert + Assert.Equal("123", result["body"]?["users"][0]?["position"]?.ToString()); + Assert.Equal("Tim", result["body"]?["users"][0]?["displayname"]?.ToString()); + } } diff --git a/tools/Tests/JsonUtilitiesTest/JsonUtilitiesTest.csproj b/tools/Tests/JsonUtilitiesTest/JsonUtilitiesTest.csproj index 2db61912cf6..d2b8100f8fa 100644 --- a/tools/Tests/JsonUtilitiesTest/JsonUtilitiesTest.csproj +++ b/tools/Tests/JsonUtilitiesTest/JsonUtilitiesTest.csproj @@ -21,6 +21,10 @@ ../../Custom/JsonExtensions.cs + ../../Custom/PreserveStringConverter.cs + + + ../../Custom/PreserveStringConverter.cs From 41fac07f0f0089918e08cf81d0a87e48a5a94c7a Mon Sep 17 00:00:00 2001 From: Microsoft Graph DevX Tooling Date: Thu, 27 Feb 2025 13:52:32 +0300 Subject: [PATCH 2/5] Made changes as per PR recommendations --- tools/Custom/JsonExtensions.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tools/Custom/JsonExtensions.cs b/tools/Custom/JsonExtensions.cs index 559bbc75c70..78db4a33e8c 100644 --- a/tools/Custom/JsonExtensions.cs +++ b/tools/Custom/JsonExtensions.cs @@ -154,10 +154,8 @@ private static void ProcessBody(JToken token) string stringValue = value.ToString(); try { - JToken parsedValue = JToken.Parse(stringValue); - string originalToken = JsonConvert.SerializeObject(parsedValue, Formatting.None, new PreserveStringConverter()); // Ensures that the value matches the original type - property.Value = originalToken; // Replace with unescaped JSON object - ProcessBody(originalToken); // Recursively process + property.Value = stringValue; // Replace with unescaped JSON object + ProcessBody(stringValue); // Recursively process } catch (Newtonsoft.Json.JsonException) { @@ -182,10 +180,8 @@ private static void ProcessBody(JToken token) string stringValue = value.ToString(); try { - JToken parsedValue = JToken.Parse(stringValue); - string originalToken = JsonConvert.SerializeObject(parsedValue, Formatting.None, new PreserveStringConverter()); // Ensures that the value matches the original type - jsonArray[i] = originalToken; // Replace with unescaped JSON object - ProcessBody(originalToken); // Recursively process + jsonArray[i] = stringValue; // Replace with unescaped JSON object + ProcessBody(stringValue); // Recursively process } catch (Newtonsoft.Json.JsonException) { From 971bb7fc8ca3ab367f294b0188ed174d3827330f Mon Sep 17 00:00:00 2001 From: Microsoft Graph DevX Tooling Date: Fri, 7 Mar 2025 11:57:22 +0300 Subject: [PATCH 3/5] maintained parsing of values to JsonToken because it unescapes the values --- tools/Custom/JsonExtensions.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/Custom/JsonExtensions.cs b/tools/Custom/JsonExtensions.cs index 78db4a33e8c..62422ca0bf8 100644 --- a/tools/Custom/JsonExtensions.cs +++ b/tools/Custom/JsonExtensions.cs @@ -131,7 +131,7 @@ public static string ReplaceAndRemoveSlashes(this string body) ProcessBody(jsonToken); // Return cleaned JSON string - return JsonConvert.SerializeObject(jsonToken, Formatting.None, new PreserveStringConverter()); + return JsonConvert.SerializeObject(jsonToken, Formatting.None); } catch (Newtonsoft.Json.JsonException) { @@ -154,7 +154,8 @@ private static void ProcessBody(JToken token) string stringValue = value.ToString(); try { - property.Value = stringValue; // Replace with unescaped JSON object + JToken parsedValue = JToken.Parse(stringValue); + property.Value = parsedValue; // Replace with unescaped JSON object ProcessBody(stringValue); // Recursively process } catch (Newtonsoft.Json.JsonException) @@ -180,7 +181,8 @@ private static void ProcessBody(JToken token) string stringValue = value.ToString(); try { - jsonArray[i] = stringValue; // Replace with unescaped JSON object + JToken parsedValue = JToken.Parse(stringValue); + jsonArray[i] = parsedValue; // Replace with unescaped JSON object ProcessBody(stringValue); // Recursively process } catch (Newtonsoft.Json.JsonException) From 8edfc35e973463b7bd294a83165271f520d6edad Mon Sep 17 00:00:00 2001 From: Microsoft Graph DevX Tooling Date: Fri, 7 Mar 2025 12:28:28 +0300 Subject: [PATCH 4/5] Removed unnecesary converter --- tools/Custom/PreserveStringConverter.cs | 31 --------- .../JsonUtilitiesTest/JsonExtensionsTests.cs | 64 ++++++------------- .../JsonUtilitiesTest.csproj | 3 - 3 files changed, 21 insertions(+), 77 deletions(-) delete mode 100644 tools/Custom/PreserveStringConverter.cs diff --git a/tools/Custom/PreserveStringConverter.cs b/tools/Custom/PreserveStringConverter.cs deleted file mode 100644 index ac4b0f2a37a..00000000000 --- a/tools/Custom/PreserveStringConverter.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Text; - -namespace NamespacePrefixPlaceholder.PowerShell.JsonUtilities -{ - public class PreserveStringConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(string); // Only applies to string properties - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - // If the value is a number but the property expects a string, return it as a string - if ((reader.TokenType == JsonToken.Integer || reader.TokenType == JsonToken.Float) && objectType == typeof(string)) - { - return reader.Value?.ToString(); - } - - return reader?.Value; // Otherwise, keep it as is - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteValue(value); // Preserve the original type - } - } -} diff --git a/tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs b/tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs index a55e634ac8d..2cd1e56e441 100644 --- a/tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs +++ b/tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs @@ -247,60 +247,38 @@ public void RemoveDefaultNullProperties_ShouldRemoveDefaultNullValuesInJsonObjec Assert.True(result["body"]?["users"][0]?["metadata"]?.ToObject().ContainsKey("phone")); } + /* + Test for unescaping json object while maintaining original property definition of values + instead of auto inferencing the type of the value + "fields": "{\r\n \"BasicTag\": \"v2.31.0\",\r\n \"BuildID\": \"3599\",\r\n \"MWSCommitID\": \"a5c7998252f2366c8cbbb03ba46e9b\",\r\n \"MWSTag\": \"v2.21.0\",\r\n \"BasicCommitID\": \"9c3d0f36362dd25caa0da2ecab06a1859ce2\",\r\n \"CustomerCommitID\": \"c40241be9fd2f1cd2f2f2fc961c37f720c\"\r\n}" + */ [Fact] - public void NumericString_RemainsString() - { + public void RemoveDefaultNullProperties_ShouldUnescapeJsonString(){ // Arrange JObject json = JObject.Parse(@"{ - ""displayname"": ""Tim"", - ""position"": ""123"", - ""salary"": 2000000 + ""fields"": ""{\r\n \""BasicTag\"": \""v2.31.0\"",\r\n \""BuildID\"": \""3599\"",\r\n \""MWSCommitID\"": \""a5c7998252f2366c8cbbb03ba46e9b\"",\r\n \""MWSTag\"": \""v2.21.0\"",\r\n \""BasicCommitID\"": \""9c3d0f36362dd25caa0da2ecab06a1859ce2\"",\r\n \""CustomerCommitID\"": \""c40241be9fd2f1cd2f2f2fc961c37f720c\""\r\n}"" }"); - // Act - string cleanedJson = json.ToString()?.ReplaceAndRemoveSlashes(); - JObject result = JObject.Parse(cleanedJson); - - // Assert - Assert.Equal("123", result["position"]?.ToString()); - Assert.Equal(2000000, result["salary"]?.ToObject()); - } - [Fact] - public void NumericString_RemainsStringInJsonArray() - { - // Arrange - JArray json = JArray.Parse(@"[ - { ""displayname"": ""Tim"", ""position"": ""123"" } - - ]"); - - // Act - string cleanedJson = json.ToString()?.ReplaceAndRemoveSlashes(); - JArray result = JArray.Parse(cleanedJson); - - // Assert - Assert.Equal("123", result[0]?["position"]?.ToString()); - } - - [Fact] - public void NumericString_RemainsStringInNestedJsonObject() - { - // Arrange - JObject json = JObject.Parse(@"{ - ""body"":{ - ""users"": [ - { ""displayname"": ""Tim"", ""position"": ""123"" } - ] + String expectedJson = @"{ + ""fields"": { + ""BasicTag"": ""v2.31.0"", + ""BuildID"": ""3599"", + ""MWSCommitID"": ""a5c7998252f2366c8cbbb03ba46e9b"", + ""MWSTag"": ""v2.21.0"", + ""BasicCommitID"": ""9c3d0f36362dd25caa0da2ecab06a1859ce2"", + ""CustomerCommitID"": ""c40241be9fd2f1cd2f2f2fc961c37f720c"" } - }"); + }"; // Act + //Convert Json object to string then pass it to RemoveAndReplaceSlashes method string cleanedJson = json.ToString()?.ReplaceAndRemoveSlashes(); - JObject result = JObject.Parse(cleanedJson); // Assert - Assert.Equal("123", result["body"]?["users"][0]?["position"]?.ToString()); - Assert.Equal("Tim", result["body"]?["users"][0]?["displayname"]?.ToString()); + Assert.Equal(NormalizeJson(expectedJson), NormalizeJson(cleanedJson)); } + + + } diff --git a/tools/Tests/JsonUtilitiesTest/JsonUtilitiesTest.csproj b/tools/Tests/JsonUtilitiesTest/JsonUtilitiesTest.csproj index d2b8100f8fa..f80e0c27680 100644 --- a/tools/Tests/JsonUtilitiesTest/JsonUtilitiesTest.csproj +++ b/tools/Tests/JsonUtilitiesTest/JsonUtilitiesTest.csproj @@ -23,9 +23,6 @@ ../../Custom/JsonExtensions.cs ../../Custom/PreserveStringConverter.cs - - ../../Custom/PreserveStringConverter.cs - From 9fadfea566d6c59370b1289fe05cbf90da7f643b Mon Sep 17 00:00:00 2001 From: Microsoft Graph DevX Tooling Date: Fri, 7 Mar 2025 12:38:16 +0300 Subject: [PATCH 5/5] Cleaned up link to unexistent file --- tools/Tests/JsonUtilitiesTest/JsonUtilitiesTest.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/Tests/JsonUtilitiesTest/JsonUtilitiesTest.csproj b/tools/Tests/JsonUtilitiesTest/JsonUtilitiesTest.csproj index f80e0c27680..2db61912cf6 100644 --- a/tools/Tests/JsonUtilitiesTest/JsonUtilitiesTest.csproj +++ b/tools/Tests/JsonUtilitiesTest/JsonUtilitiesTest.csproj @@ -21,7 +21,6 @@ ../../Custom/JsonExtensions.cs - ../../Custom/PreserveStringConverter.cs