diff --git a/tools/Custom/JsonExtensions.cs b/tools/Custom/JsonExtensions.cs index a951c0797a3..08aa3628a10 100644 --- a/tools/Custom/JsonExtensions.cs +++ b/tools/Custom/JsonExtensions.cs @@ -1,6 +1,7 @@ namespace NamespacePrefixPlaceholder.PowerShell.JsonUtilities { using Newtonsoft.Json.Linq; + using Newtonsoft.Json; using System; using System.Linq; @@ -20,66 +21,181 @@ public static class JsonExtensions /// Console.WriteLine(cleanedJson); /// // Output: { "name": "John", "address": null } /// + public static string RemoveDefaultNullProperties(this JToken token) { try { - if (token is JObject jsonObject) + ProcessToken(token); + + // If the root token is completely empty, return "{}" or "[]" + if (token is JObject obj && !obj.HasValues) return "{}"; + if (token is JArray arr && !arr.HasValues) return "[]"; + + return token.ToString(); + } + catch (Exception) + { + return token.ToString(); // Return original JSON if an error occurs + } + } + + private static JToken ProcessToken(JToken token) + { + if (token is JObject jsonObject) + { + // Remove properties with "defaultnull" but keep valid ones + var propertiesToRemove = jsonObject.Properties() + .Where(p => p.Value.Type == JTokenType.String && p.Value.ToString().Equals("defaultnull", StringComparison.Ordinal)) + .ToList(); + + foreach (var property in propertiesToRemove) + { + property.Remove(); + } + + // Recursively process remaining properties + foreach (var property in jsonObject.Properties().ToList()) + { + JToken cleanedValue = ProcessToken(property.Value); + + // Convert explicit "null" strings to actual null + if (property.Value.Type == JTokenType.String && property.Value.ToString().Equals("null", StringComparison.Ordinal)) + { + property.Value = JValue.CreateNull(); + } + + // Remove the property if it's now empty after processing + if (ShouldRemove(cleanedValue)) + { + property.Remove(); + } + } + + // Remove the object itself if ALL properties are removed (empty object) + return jsonObject.HasValues ? jsonObject : null; + } + else if (token is JArray jsonArray) + { + for (int i = jsonArray.Count - 1; i >= 0; i--) { - foreach (var property in jsonObject.Properties().ToList()) + JToken item = jsonArray[i]; + + // Process nested objects/arrays inside the array + if (item is JObject || item is JArray) { - if (property.Value.Type == JTokenType.Object) + JToken cleanedItem = ProcessToken(item); + + if (ShouldRemove(cleanedItem)) { - RemoveDefaultNullProperties(property.Value); + jsonArray.RemoveAt(i); // Remove empty or unnecessary items } - else if (property.Value.Type == JTokenType.Array) + else { - RemoveDefaultNullProperties(property.Value); + jsonArray[i] = cleanedItem; // Update with cleaned version } - else if (property.Value.Type == JTokenType.String && property.Value.ToString().Equals("defaultnull",StringComparison.Ordinal)) + } + else if (item.Type == JTokenType.String && item.ToString().Equals("null", StringComparison.Ordinal)) + { + jsonArray[i] = JValue.CreateNull(); // Convert "null" string to JSON null + } + else if (item.Type == JTokenType.String && item.ToString().Equals("defaultnull", StringComparison.Ordinal)) + { + jsonArray.RemoveAt(i); // Remove "defaultnull" entries + } + } + + return jsonArray.HasValues ? jsonArray : null; + } + + return token; + } + + private static bool ShouldRemove(JToken token) + { + return token == null || + (token.Type == JTokenType.Object && !token.HasValues) || // Remove empty objects + (token.Type == JTokenType.Array && !token.HasValues); // Remove empty arrays + } + + + public static string ReplaceAndRemoveSlashes(this string body) + { + try + { + // Parse the JSON using Newtonsoft.Json + JToken jsonToken = JToken.Parse(body); + if (jsonToken == null) return body; // If parsing fails, return original body + + // Recursively process JSON to remove escape sequences + ProcessBody(jsonToken); + + // Return cleaned JSON string + return JsonConvert.SerializeObject(jsonToken, Formatting.None); + } + catch (Newtonsoft.Json.JsonException) + { + // If it's not valid JSON, apply normal string replacements + return body.Replace("\\", "").Replace("rn", "").Replace("\"{", "{").Replace("}\"", "}"); + } + } + + private static void ProcessBody(JToken token) + { + if (token is JObject jsonObject) + { + foreach (var property in jsonObject.Properties().ToList()) + { + var value = property.Value; + + // If the value is a string, attempt to parse it as JSON to remove escaping + if (value.Type == JTokenType.String) + { + string stringValue = value.ToString(); + try { - property.Remove(); + JToken parsedValue = JToken.Parse(stringValue); + property.Value = parsedValue; // Replace with unescaped JSON object + ProcessBody(parsedValue); // Recursively process } - else if (property.Value.Type == JTokenType.String && property.Value.ToString().Equals("null",StringComparison.Ordinal)) + catch (Newtonsoft.Json.JsonException) { - property.Value = JValue.CreateNull(); + // If parsing fails, leave the value as is } } + else if (value is JObject || value is JArray) + { + ProcessBody(value); // Recursively process nested objects/arrays + } } - else if (token is JArray jsonArray) + } + else if (token is JArray jsonArray) + { + for (int i = 0; i < jsonArray.Count; i++) { - // Process each item in the JArray - for (int i = jsonArray.Count - 1; i >= 0; i--) - { - var item = jsonArray[i]; + var value = jsonArray[i]; - if (item.Type == JTokenType.Object) - { - RemoveDefaultNullProperties(item); - } - else if (item.Type == JTokenType.String && item.ToString().Equals("defaultnull",StringComparison.Ordinal)) + // If the value is a string, attempt to parse it as JSON to remove escaping + if (value.Type == JTokenType.String) + { + string stringValue = value.ToString(); + try { - jsonArray.RemoveAt(i); // Remove the "defaultnull" string from the array + JToken parsedValue = JToken.Parse(stringValue); + jsonArray[i] = parsedValue; // Replace with unescaped JSON object + ProcessBody(parsedValue); // Recursively process } - else if (item.Type == JTokenType.String && item.ToString().Equals("null",StringComparison.Ordinal)) + catch (Newtonsoft.Json.JsonException) { - jsonArray[i] = JValue.CreateNull(); // Convert "null" string to actual null + // If parsing fails, leave the value as is } } + else if (value is JObject || value is JArray) + { + ProcessBody(value); // Recursively process nested objects/arrays + } } } - catch (System.Exception ex) - { - Console.WriteLine($"Error cleaning JSON: {ex.Message}"); - return token.ToString(); // Return the original JSON if any error occurs - } - - return token.ToString(); - } - - public static string ReplaceAndRemoveSlashes(this string body) - { - return body.Replace("\\", "").Replace("rn", "").Replace("\"{", "{").Replace("}\"", "}"); } } -} \ No newline at end of file +} diff --git a/tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs b/tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs index 2b25e1b1a85..1ba339600e9 100644 --- a/tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs +++ b/tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs @@ -57,9 +57,22 @@ public void RemoveDefaultNullProperties_ShouldHandleNestedObjects() // Arrange JObject json = JObject.Parse(@"{ ""displayname"": ""Tim"", + ""professions"": { + }, + ""jobProfile"": { + ""dept"": ""ICT"", + ""manager"": false, + ""supervisor"" : ""defaultnull"" + }, ""metadata"": { ""phone"": ""defaultnull"", - ""location"": ""Nairobi"" + ""location"": ""null"", + ""address"": { + ""city"": ""Nairobi"", + ""street"": ""defaultnull"" + }, + ""station"": { + } } }"); @@ -69,7 +82,12 @@ public void RemoveDefaultNullProperties_ShouldHandleNestedObjects() // Assert Assert.False(result["metadata"]?.ToObject()?.ContainsKey("phone")); - Assert.Equal("Nairobi", result["metadata"]?["location"]?.ToString()); + Assert.Equal("ICT", result["jobProfile"]?["dept"]?.ToString()); + Assert.Equal("Nairobi", result["metadata"]?["address"]?["city"]?.ToString()); + Assert.Null(result["metadata"]?["location"]?.Value()); + // Check if emptynested object is removed + Assert.False(result["metadata"]?.ToObject()?.ContainsKey("station ")); + Assert.False(result?.ToObject()?.ContainsKey("professions")); } [Fact] @@ -206,6 +224,27 @@ private string NormalizeJson(string json) }); } + [Fact] + public void RemoveDefaultNullProperties_ShouldRemoveDefaultNullValuesInJsonObjectWithBothDeeplyNestedObjectsAndArrays(){ + // Arrange + JObject json = JObject.Parse(@"{ + ""body"":{ + ""users"": [ + { ""displayname"": ""Tim"", ""email"": ""defaultnull"", ""metadata"": { ""phone"": ""254714390915"" } } + ] + }, + ""users"": [ + { ""displayname"": ""Tim"", ""email"": ""defaultnull"", ""metadata"": { ""phone"": ""254714390915"" } }]}"); + + // Act + string cleanedJson = json.RemoveDefaultNullProperties(); + JObject result = JObject.Parse(cleanedJson); + // Assert + Assert.False(result["users"][0]?.ToObject().ContainsKey("email")); + Assert.True(result["users"][0]?["metadata"]?.ToObject().ContainsKey("phone")); + Assert.False(result["body"]?["users"][0]?.ToObject().ContainsKey("email")); + Assert.True(result["body"]?["users"][0]?["metadata"]?.ToObject().ContainsKey("phone")); + } }