Skip to content

Commit 0c2566e

Browse files
authored
Merge pull request #3164 from microsoftgraph/fix/nested-json-objs
Updated Json extension class to handle deeply nested object and array
2 parents 1bb5aa6 + 45b34e8 commit 0c2566e

File tree

2 files changed

+193
-38
lines changed

2 files changed

+193
-38
lines changed

tools/Custom/JsonExtensions.cs

Lines changed: 152 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace NamespacePrefixPlaceholder.PowerShell.JsonUtilities
22
{
33
using Newtonsoft.Json.Linq;
4+
using Newtonsoft.Json;
45
using System;
56
using System.Linq;
67

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

56-
if (item.Type == JTokenType.Object)
57-
{
58-
RemoveDefaultNullProperties(item);
59-
}
60-
else if (item.Type == JTokenType.String && item.ToString().Equals("defaultnull",StringComparison.Ordinal))
178+
// If the value is a string, attempt to parse it as JSON to remove escaping
179+
if (value.Type == JTokenType.String)
180+
{
181+
string stringValue = value.ToString();
182+
try
61183
{
62-
jsonArray.RemoveAt(i); // Remove the "defaultnull" string from the array
184+
JToken parsedValue = JToken.Parse(stringValue);
185+
jsonArray[i] = parsedValue; // Replace with unescaped JSON object
186+
ProcessBody(parsedValue); // Recursively process
63187
}
64-
else if (item.Type == JTokenType.String && item.ToString().Equals("null",StringComparison.Ordinal))
188+
catch (Newtonsoft.Json.JsonException)
65189
{
66-
jsonArray[i] = JValue.CreateNull(); // Convert "null" string to actual null
190+
// If parsing fails, leave the value as is
67191
}
68192
}
193+
else if (value is JObject || value is JArray)
194+
{
195+
ProcessBody(value); // Recursively process nested objects/arrays
196+
}
69197
}
70198
}
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("rn", "").Replace("\"{", "{").Replace("}\"", "}");
83199
}
84200
}
85-
}
201+
}

tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,22 @@ public void RemoveDefaultNullProperties_ShouldHandleNestedObjects()
5757
// Arrange
5858
JObject json = JObject.Parse(@"{
5959
""displayname"": ""Tim"",
60+
""professions"": {
61+
},
62+
""jobProfile"": {
63+
""dept"": ""ICT"",
64+
""manager"": false,
65+
""supervisor"" : ""defaultnull""
66+
},
6067
""metadata"": {
6168
""phone"": ""defaultnull"",
62-
""location"": ""Nairobi""
69+
""location"": ""null"",
70+
""address"": {
71+
""city"": ""Nairobi"",
72+
""street"": ""defaultnull""
73+
},
74+
""station"": {
75+
}
6376
}
6477
}");
6578

@@ -69,7 +82,12 @@ public void RemoveDefaultNullProperties_ShouldHandleNestedObjects()
6982

7083
// Assert
7184
Assert.False(result["metadata"]?.ToObject<JObject>()?.ContainsKey("phone"));
72-
Assert.Equal("Nairobi", result["metadata"]?["location"]?.ToString());
85+
Assert.Equal("ICT", result["jobProfile"]?["dept"]?.ToString());
86+
Assert.Equal("Nairobi", result["metadata"]?["address"]?["city"]?.ToString());
87+
Assert.Null(result["metadata"]?["location"]?.Value<string>());
88+
// Check if emptynested object is removed
89+
Assert.False(result["metadata"]?.ToObject<JObject>()?.ContainsKey("station "));
90+
Assert.False(result?.ToObject<JObject>()?.ContainsKey("professions"));
7391
}
7492

7593
[Fact]
@@ -206,6 +224,27 @@ private string NormalizeJson(string json)
206224
});
207225
}
208226

227+
[Fact]
228+
public void RemoveDefaultNullProperties_ShouldRemoveDefaultNullValuesInJsonObjectWithBothDeeplyNestedObjectsAndArrays(){
229+
// Arrange
230+
JObject json = JObject.Parse(@"{
231+
""body"":{
232+
""users"": [
233+
{ ""displayname"": ""Tim"", ""email"": ""defaultnull"", ""metadata"": { ""phone"": ""254714390915"" } }
234+
]
235+
},
236+
""users"": [
237+
{ ""displayname"": ""Tim"", ""email"": ""defaultnull"", ""metadata"": { ""phone"": ""254714390915"" } }]}");
238+
239+
// Act
240+
string cleanedJson = json.RemoveDefaultNullProperties();
241+
JObject result = JObject.Parse(cleanedJson);
209242

243+
// Assert
244+
Assert.False(result["users"][0]?.ToObject<JObject>().ContainsKey("email"));
245+
Assert.True(result["users"][0]?["metadata"]?.ToObject<JObject>().ContainsKey("phone"));
246+
Assert.False(result["body"]?["users"][0]?.ToObject<JObject>().ContainsKey("email"));
247+
Assert.True(result["body"]?["users"][0]?["metadata"]?.ToObject<JObject>().ContainsKey("phone"));
248+
}
210249
}
211250

0 commit comments

Comments
 (0)