Skip to content

Commit f2a809c

Browse files
Microsoft Graph DevX ToolingMicrosoft Graph DevX Tooling
authored andcommitted
Updated Json extension to handle deeply nested object and array
1 parent b0bcddf commit f2a809c

File tree

2 files changed

+200
-42
lines changed

2 files changed

+200
-42
lines changed

tools/Custom/JsonExtensions.cs

Lines changed: 159 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
namespace NamespacePrefixPlaceholder.PowerShell.JsonUtilities
22
{
33
using Newtonsoft.Json.Linq;
4+
using Newtonsoft.Json;
45
using System;
56
using System.Linq;
7+
using System.Text.Json;
8+
using System.Text.Json.Nodes;
69

710
public static class JsonExtensions
811
{
@@ -20,66 +23,182 @@ public static class JsonExtensions
2023
/// Console.WriteLine(cleanedJson);
2124
/// // Output: { "name": "John", "address": null }
2225
/// </example>
26+
2327
public static string RemoveDefaultNullProperties(this JToken token)
2428
{
2529
try
2630
{
27-
if (token is JObject jsonObject)
31+
ProcessToken(token);
32+
33+
// If the root token is completely empty, return "{}" or "[]"
34+
if (token is JObject obj && !obj.HasValues) return "{}";
35+
if (token is JArray arr && !arr.HasValues) return "[]";
36+
37+
return token.ToString();
38+
}
39+
catch (Exception)
40+
{
41+
return token.ToString(); // Return original JSON if an error occurs
42+
}
43+
}
44+
45+
private static JToken ProcessToken(JToken token)
46+
{
47+
if (token is JObject jsonObject)
48+
{
49+
// Remove properties with "defaultnull" but keep valid ones
50+
var propertiesToRemove = jsonObject.Properties()
51+
.Where(p => p.Value.Type == JTokenType.String && p.Value.ToString().Equals("defaultnull", StringComparison.Ordinal))
52+
.ToList();
53+
54+
foreach (var property in propertiesToRemove)
2855
{
29-
foreach (var property in jsonObject.Properties().ToList())
56+
property.Remove();
57+
}
58+
59+
// Recursively process remaining properties
60+
foreach (var property in jsonObject.Properties().ToList())
61+
{
62+
JToken cleanedValue = ProcessToken(property.Value);
63+
64+
// Convert explicit "null" strings to actual null
65+
if (property.Value.Type == JTokenType.String && property.Value.ToString().Equals("null", StringComparison.Ordinal))
3066
{
31-
if (property.Value.Type == JTokenType.Object)
32-
{
33-
RemoveDefaultNullProperties(property.Value);
34-
}
35-
else if (property.Value.Type == JTokenType.Array)
36-
{
37-
RemoveDefaultNullProperties(property.Value);
38-
}
39-
else if (property.Value.Type == JTokenType.String && property.Value.ToString().Equals("defaultnull",StringComparison.Ordinal))
67+
property.Value = JValue.CreateNull();
68+
}
69+
70+
// Remove the property if it's now empty after processing
71+
if (ShouldRemove(cleanedValue))
72+
{
73+
property.Remove();
74+
}
75+
}
76+
77+
// Remove the object itself if ALL properties are removed (empty object)
78+
return jsonObject.HasValues ? jsonObject : null;
79+
}
80+
else if (token is JArray jsonArray)
81+
{
82+
for (int i = jsonArray.Count - 1; i >= 0; i--)
83+
{
84+
JToken item = jsonArray[i];
85+
86+
// Process nested objects/arrays inside the array
87+
if (item is JObject || item is JArray)
88+
{
89+
JToken cleanedItem = ProcessToken(item);
90+
91+
if (ShouldRemove(cleanedItem))
92+
{
93+
jsonArray.RemoveAt(i); // Remove empty or unnecessary items
94+
}
95+
else
96+
{
97+
jsonArray[i] = cleanedItem; // Update with cleaned version
98+
}
99+
}
100+
else if (item.Type == JTokenType.String && item.ToString().Equals("null", StringComparison.Ordinal))
101+
{
102+
jsonArray[i] = JValue.CreateNull(); // Convert "null" string to JSON null
103+
}
104+
else if (item.Type == JTokenType.String && item.ToString().Equals("defaultnull", StringComparison.Ordinal))
105+
{
106+
jsonArray.RemoveAt(i); // Remove "defaultnull" entries
107+
}
108+
}
109+
110+
return jsonArray.HasValues ? jsonArray : null;
111+
}
112+
113+
return token;
114+
}
115+
116+
private static bool ShouldRemove(JToken token)
117+
{
118+
return token == null ||
119+
(token.Type == JTokenType.Object && !token.HasValues) || // Remove empty objects
120+
(token.Type == JTokenType.Array && !token.HasValues); // Remove empty arrays
121+
}
122+
123+
124+
public static string ReplaceAndRemoveSlashes(this string body)
125+
{
126+
try
127+
{
128+
// Parse the JSON using Newtonsoft.Json
129+
JToken jsonToken = JToken.Parse(body);
130+
if (jsonToken == null) return body; // If parsing fails, return original body
131+
132+
// Recursively process JSON to remove escape sequences
133+
ProcessBody(jsonToken);
134+
135+
// Return cleaned JSON string
136+
return JsonConvert.SerializeObject(jsonToken, Formatting.None);
137+
}
138+
catch (Newtonsoft.Json.JsonException)
139+
{
140+
// If it's not valid JSON, apply normal string replacements
141+
return body.Replace("\\", "").Replace("rn", "").Replace("\"{", "{").Replace("}\"", "}");
142+
}
143+
}
144+
145+
private static void ProcessBody(JToken token)
146+
{
147+
if (token is JObject jsonObject)
148+
{
149+
foreach (var property in jsonObject.Properties().ToList())
150+
{
151+
var value = property.Value;
152+
153+
// If the value is a string, attempt to parse it as JSON to remove escaping
154+
if (value.Type == JTokenType.String)
155+
{
156+
string stringValue = value.ToString();
157+
try
40158
{
41-
property.Remove();
159+
JToken parsedValue = JToken.Parse(stringValue);
160+
property.Value = parsedValue; // Replace with unescaped JSON object
161+
ProcessBody(parsedValue); // Recursively process
42162
}
43-
else if (property.Value.Type == JTokenType.String && property.Value.ToString().Equals("null",StringComparison.Ordinal))
163+
catch (Newtonsoft.Json.JsonException)
44164
{
45-
property.Value = JValue.CreateNull();
165+
// If parsing fails, leave the value as is
46166
}
47167
}
168+
else if (value is JObject || value is JArray)
169+
{
170+
ProcessBody(value); // Recursively process nested objects/arrays
171+
}
48172
}
49-
else if (token is JArray jsonArray)
173+
}
174+
else if (token is JArray jsonArray)
175+
{
176+
for (int i = 0; i < jsonArray.Count; i++)
50177
{
51-
// Process each item in the JArray
52-
for (int i = jsonArray.Count - 1; i >= 0; i--)
53-
{
54-
var item = jsonArray[i];
178+
var value = jsonArray[i];
55179

56-
if (item.Type == JTokenType.Object)
57-
{
58-
RemoveDefaultNullProperties(item);
59-
}
60-
else if (item.Type == JTokenType.String && item.ToString().Equals("defaultnull",StringComparison.Ordinal))
180+
// If the value is a string, attempt to parse it as JSON to remove escaping
181+
if (value.Type == JTokenType.String)
182+
{
183+
string stringValue = value.ToString();
184+
try
61185
{
62-
jsonArray.RemoveAt(i); // Remove the "defaultnull" string from the array
186+
JToken parsedValue = JToken.Parse(stringValue);
187+
jsonArray[i] = parsedValue; // Replace with unescaped JSON object
188+
ProcessBody(parsedValue); // Recursively process
63189
}
64-
else if (item.Type == JTokenType.String && item.ToString().Equals("null",StringComparison.Ordinal))
190+
catch (Newtonsoft.Json.JsonException)
65191
{
66-
jsonArray[i] = JValue.CreateNull(); // Convert "null" string to actual null
192+
// If parsing fails, leave the value as is
67193
}
68194
}
195+
else if (value is JObject || value is JArray)
196+
{
197+
ProcessBody(value); // Recursively process nested objects/arrays
198+
}
69199
}
70200
}
71-
catch (System.Exception ex)
72-
{
73-
Console.WriteLine($"Error cleaning JSON: {ex.Message}");
74-
return token.ToString(); // Return the original JSON if any error occurs
75-
}
76-
77-
return token.ToString();
78-
}
79-
80-
public static string ReplaceAndRemoveSlashes(this string body)
81-
{
82-
return body.Replace("/", "").Replace("\\", "").Replace("rn", "").Replace("\"{", "{").Replace("}\"", "}");
83201
}
84202
}
85-
}
203+
}
204+

tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,22 @@ public void RemoveDefaultNullProperties_ShouldHandleNestedObjects()
5656
// Arrange
5757
JObject json = JObject.Parse(@"{
5858
""displayname"": ""Tim"",
59+
""professions"": {
60+
},
61+
""passwordProfile"": {
62+
""password"": ""pass123"",
63+
""forceChangePasswordNextSignIn"": false,
64+
""forcePassWitMfa"" : ""defaultnull""
65+
},
5966
""metadata"": {
6067
""phone"": ""defaultnull"",
61-
""location"": ""Nairobi""
68+
""location"": ""null"",
69+
""address"": {
70+
""city"": ""Nairobi"",
71+
""street"": ""defaultnull""
72+
},
73+
""station"": {
74+
}
6275
}
6376
}");
6477

@@ -68,7 +81,12 @@ public void RemoveDefaultNullProperties_ShouldHandleNestedObjects()
6881

6982
// Assert
7083
Assert.False(result["metadata"]?.ToObject<JObject>()?.ContainsKey("phone"));
71-
Assert.Equal("Nairobi", result["metadata"]?["location"]?.ToString());
84+
Assert.Equal("pass123", result["passwordProfile"]?["password"]?.ToString());
85+
Assert.Equal("Nairobi", result["metadata"]?["address"]?["city"]?.ToString());
86+
Assert.Null(result["metadata"]?["location"]?.Value<string>());
87+
// Check if emptynested object is removed
88+
Assert.False(result["metadata"]?.ToObject<JObject>()?.ContainsKey("station "));
89+
Assert.False(result?.ToObject<JObject>()?.ContainsKey("professions"));
7290
}
7391

7492
[Fact]
@@ -145,6 +163,27 @@ public void RemoveDefaultNullProperties_ShouldRemoveDefaultNullValuesInJsonArray
145163

146164
}
147165

166+
[Fact]
167+
public void RemoveDefaultNullProperties_ShouldRemoveDefaultNullValuesInJsonObjectWithBothDeeplyNestedObjectsAndArrays(){
168+
// Arrange
169+
JObject json = JObject.Parse(@"{
170+
""body"":{
171+
""users"": [
172+
{ ""displayname"": ""Tim"", ""email"": ""defaultnull"", ""metadata"": { ""phone"": ""254714390915"" } }
173+
]
174+
},
175+
""users"": [
176+
{ ""displayname"": ""Tim"", ""email"": ""defaultnull"", ""metadata"": { ""phone"": ""254714390915"" } }]}");
177+
178+
// Act
179+
string cleanedJson = json.RemoveDefaultNullProperties();
180+
JObject result = JObject.Parse(cleanedJson);
148181

182+
// Assert
183+
Assert.False(result["users"][0]?.ToObject<JObject>().ContainsKey("email"));
184+
Assert.True(result["users"][0]?["metadata"]?.ToObject<JObject>().ContainsKey("phone"));
185+
Assert.False(result["body"]?["users"][0]?.ToObject<JObject>().ContainsKey("email"));
186+
Assert.True(result["body"]?["users"][0]?["metadata"]?.ToObject<JObject>().ContainsKey("phone"));
187+
}
149188
}
150189

0 commit comments

Comments
 (0)