Skip to content

Commit 46a4bfd

Browse files
authored
Merge pull request #3224 from microsoftgraph/property-tracker
Fixes removal of boolean and empty Json object values from request body
2 parents 3cf23b5 + 8234341 commit 46a4bfd

File tree

8 files changed

+217
-242
lines changed

8 files changed

+217
-242
lines changed

src/Teams/beta/custom/MicrosoftGraphRscConfiguration.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ public partial class MicrosoftGraphRscConfiguration :
1515
IMicrosoftGraphRscConfigurationInternal,
1616
Runtime.IValidates
1717
{
18+
private readonly PropertyTracker _propertyTracker = new PropertyTracker();
19+
public void TrackProperty(string propertyName) => _propertyTracker.TrackProperty(propertyName);
20+
public bool IsPropertySet(string propertyName) =>_propertyTracker.IsPropertySet(propertyName);
21+
public T SanitizeValue<T>(object value) => PropertyTracker.SanitizeValue<T>(value);
1822
/// <summary>
1923
/// Backing field for Inherited model <see cref= "Microsoft.Graph.Beta.PowerShell.Models.IMicrosoftGraphEntity" />
2024
/// </summary>

src/Teams/beta/custom/MicrosoftGraphTeamsAppPreApproval.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ public partial class MicrosoftGraphTeamsAppPreApproval :
1515
IMicrosoftGraphTeamsAppPreApprovalInternal,
1616
Runtime.IValidates
1717
{
18+
private readonly PropertyTracker _propertyTracker = new PropertyTracker();
19+
public void TrackProperty(string propertyName) => _propertyTracker.TrackProperty(propertyName);
20+
public bool IsPropertySet(string propertyName) =>_propertyTracker.IsPropertySet(propertyName);
21+
public T SanitizeValue<T>(object value) => PropertyTracker.SanitizeValue<T>(value);
22+
1823
/// <summary>
1924
/// Backing field for Inherited model <see cref= "Microsoft.Graph.Beta.PowerShell.Models.IMicrosoftGraphEntity" />
2025
/// </summary>

src/readme.graph.md

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ directive:
305305
- from: source-file-csharp
306306
where: $
307307
transform: >
308-
if (!$documentPath.match(/generated%2Fapi%2FModels%2F\w*MicrosoftGraph\w*\d*.json.cs/gm))
308+
if (!$documentPath.match(/generated%2Fapi%2FModels%2F\w*\d*.json.cs/gm))
309309
{
310310
return $;
311311
} else {
@@ -333,11 +333,26 @@ directive:
333333
// Ensure dateTime is always serialized as Utc.
334334
let dateTimeToJsonRegex = /(\.Json\.JsonString\()(.*)\?(\.ToString\(@"yyyy'-'MM'-'dd'T'HH':'mm':'ss\.fffffffK")/gm
335335
$ = $.replace(dateTimeToJsonRegex, '$1System.DateTime.SpecifyKind($2.Value.ToUniversalTime(), System.DateTimeKind.Utc)$3');
336+
337+
//The following regex below adds a property tracker to ensure that users can also pass $Null as an alternative to the current "null" string which gets inferred to null.
336338
337-
// Enables null valued properties
338-
$ = $.replace(/AddIf\(\s*null\s*!=\s*(this\._\w+)\s*\?\s*\(\s*Microsoft\.Graph\.PowerShell\.Runtime\.Json\.JsonNode\)\s*(.*)\s*:\s*null\s*,\s*"(.*?)"\s*,\s*container\.Add\s*\)/gm, 'container.Add("$3", $1 != null ? (Microsoft.Graph.PowerShell.Runtime.Json.JsonNode) $2 :"defaultnull")')
339+
const regexP = /AddIf\(\s*null\s*!=\s*\(\(\(object\)this\._(\w+).*?(\(Microsoft.*.PowerShell\.Runtime\.Json\.JsonNode\)).*?"(\w+)".*?container\.Add\s*\);/gm
340+
$ = $.replace(regexP, (match, p1, p2, p3) => {
341+
let capitalizedP1 = p1.charAt(0).toUpperCase() + p1.slice(1); // Capitalize first letter
342+
return `if(this.IsPropertySet("${p1}"))\n\t\t{\n\t\t\tvar propertyInfo = this.GetType().GetProperty("${capitalizedP1}");\n\t\t\tif (propertyInfo != null)\n\t\t\t{\n\t\t\tSystem.Type propertyType = propertyInfo.PropertyType;\n\t\t\t\t\tAddIf(${p2}PropertyTracker.ConvertToJsonNode(propertyType, this._${p1}),"${p1}",container.Add);\n\t\t\t}\n\t\t}`;
343+
});
344+
345+
$ = $.replace(/if\s*\(\s*null\s*!=\s*this\._(\w+)\s*\)/gm, 'if(this.IsPropertySet("$1"))')
346+
347+
let nameSpacePrefixRegex = /(Microsoft(?:\.\w+)*?\.PowerShell)/gm
348+
let nameSpacePrefix = 'Microsoft.Graph.PowerShell';
349+
if($.match(nameSpacePrefixRegex)){
350+
let prefixMatch = nameSpacePrefixRegex.exec($);
351+
nameSpacePrefix = prefixMatch[1];
352+
}
353+
$ = $.replace(/container\.Add\("(\w+)",\s*(__\w+)\);/gm, 'var nullFlag = ('+nameSpacePrefix+'.Runtime.Json.JsonNode)new '+nameSpacePrefix+'.Runtime.Json.JsonString("nullarray");\n\t\tif($2.Count == 0)\n\t\t{\n\t\t\t$2.Add(nullFlag);\n\t\t}\n\t\tcontainer.Add("$1", $2);');
339354
340-
$ = $.replace(/AddIf\(\s*null\s*!=\s*\(\(\(\(object\)\s*(this\._\w+)\)\)?.ToString\(\)\)\s*\?\s*\(\s*Microsoft\.Graph\.PowerShell\.Runtime\.Json\.JsonNode\)\s*new\s*Microsoft\.Graph\.PowerShell\.Runtime\.Json\.JsonString\((this\._\w+).ToString\(\)\)\s*:\s*null\s*,\s*"(.*?)"\s*,\s*container\.Add\s*\)/gm, 'container.Add("$3", $1 != null ? (Microsoft.Graph.PowerShell.Runtime.Json.JsonNode) new Microsoft.Graph.PowerShell.Runtime.Json.JsonString($2.ToString()) :"defaultnull")');
355+
$ =$.replace(/AddIf\(\s+null\s+!=\s+(this\._\w+)\s+\?\s+\((Microsoft\.Graph\..*?)\)\s+this\._(\w+)\.ToJson\(null,serializationMode\)\s+:\s+null,\s+"\w+"\s+,container.Add\s+\);/gm, 'if (this.IsPropertySet("$3")) \n{\n if ($1 != null)\n{\n container.Add("$3", ($2)$1.ToJson(null, serializationMode)); \n}\nelse\n{\n container.Add("$3", "null"); \n}\n}');
341356
342357
return $;
343358
}
@@ -395,7 +410,7 @@ directive:
395410
- from: source-file-csharp
396411
where: $
397412
transform: >
398-
if (!$documentPath.match(/generated%2Fapi%2FModels%2F\w*MicrosoftGraph\w*\d*.cs/gm))
413+
if (!$documentPath.match(/generated%2Fapi%2FModels%2F\w*\d*.cs/gm))
399414
{
400415
return $;
401416
} else {
@@ -404,8 +419,31 @@ directive:
404419
if($.match(additionalPropertiesRegex)) {
405420
$ = $.replace(additionalPropertiesRegex, '$1$2 new $3');
406421
}
422+
//The following regex below adds a property tracker to ensure that users can also pass $Null as an alternative to the current "null" string which gets inferred to null.
423+
$ = $.replace(/\bpublic\s+(\w+\??)\s+(\w+)\s*{\s*get\s*=>\s*this\.(\w+);\s*set\s*=>\s*this\.\3\s*=\s*value;\s*}/gmi,'public $1 $2\n\t{\n\t\tget=>this.$3;\n\t\tset\n\t\t{\n\t\t\tthis.$3=SanitizeValue<$1>(value);\n\t\t\tTrackProperty(nameof($2));\n\t\t}\n\t}')
424+
425+
$ = $.replace(/\bpublic\s+(\w+\[\])\s+(\w+)\s*{\s*get\s*=>\s*this\.(\w+);\s*set\s*=>\s*this\.\3\s*=\s*value;\s*}/gm,'public $1 $2\n\t{\n\t\tget=>this.$3;\n\t\tset\n\t\t{\n\t\t\tthis.$3=value;\n\t\t\tTrackProperty(nameof($2));\n\t\t}\n\t}')
426+
427+
$ = $.replace(/\bpublic\s+(Microsoft\.Graph\.[\w.]+\[\])\s+(\w+)\s*{\s*get\s*=>\s*this\.(\w+);\s*set\s*=>\s*this\.\3\s*=\s*value;\s*}/gm,'public $1 $2\n\t{\n\t\tget=>this.$3;\n\t\tset\n\t\t{\n\t\t\tthis.$3=value;\n\t\t\tTrackProperty(nameof($2));\n\t\t}\n\t}')
428+
429+
const match = $documentPath.match(/generated%2Fapi%2FModels%2F([\w]*[\w\d]*)\.cs/gm);
430+
if (match) {
431+
let fileName = match[0];
432+
fileName = fileName.replace('generated%2Fapi%2FModels%2F','')
433+
fileName = fileName.replace('.cs','')
434+
const interfaceName = 'I'+fileName
435+
$ = $.replace('interface '+interfaceName+' :', 'interface '+interfaceName+' : IPropertyTracker,')
436+
const className = fileName
437+
const regexP = new RegExp(`public\\s+partial\\s+class\\s+${className}\\s*:\\s*[\\s\\S]*?{`, "gm");
438+
var matches = regexP.exec($);
439+
let originalMatch = matches[0];
440+
$ = $.replace(regexP, originalMatch+'\n\t\tprivate readonly PropertyTracker _propertyTracker = new PropertyTracker();\n\t\tpublic void TrackProperty(string propertyName) => _propertyTracker.TrackProperty(propertyName);\n\t\tpublic bool IsPropertySet(string propertyName) =>_propertyTracker.IsPropertySet(propertyName);\n\t\tpublic T SanitizeValue<T>(object value) => PropertyTracker.SanitizeValue<T>(value);');
441+
}
442+
443+
$ = $.replace(/public\s+(Microsoft\.Graph\..*?)\s+(\w+)\s+{\s+get\s+=>\s+\(\s*this\.(\w+)\s+=\s*this\.\3\s+\?\?\s+new\s+(Microsoft\.Graph\..*?)\s+set\s+=>\s+this._\w+\s+=\s+value;\s+}/gm, 'public $1 $2 { \n get => (this.$3 = this.$3 ?? new $4\n set\n {\n this.$3 = value;\n TrackProperty(nameof($2));\n }\n}')
407444
408445
return $;
446+
409447
}
410448
# Modify generated .cs cmdlets.
411449
- from: source-file-csharp
@@ -658,6 +696,7 @@ directive:
658696
659697
$ = $.replace(/request\.Content\s*=\s*new\s+global::System\.Net\.Http\.StringContent\(\s*null\s*!=\s*body\s*\?\s*new\s+Microsoft\.Graph\.Beta\.PowerShell\.Runtime\.Json\.XNodeArray\(.*?\)\s*:\s*null,\s*global::System\.Text\.Encoding\.UTF8\);/g,'request.Content = new global::System.Net.Http.StringContent(cleanedBody, global::System.Text.Encoding.UTF8);');
660698
699+
$ = $.replace(/cleanedBody = Microsoft.*.ReplaceAndRemoveSlashes\(cleanedBody\);/gm,'')
661700
return $
662701
}
663702

tools/Custom/IPropertyTracker.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace NamespacePrefixPlaceholder.PowerShell.Models
2+
{
3+
public interface IPropertyTracker
4+
{
5+
void TrackProperty(string propertyName);
6+
bool IsPropertySet(string propertyName);
7+
T SanitizeValue<T>(object value);
8+
}
9+
}

tools/Custom/JsonExtensions.cs

Lines changed: 16 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,16 @@ namespace NamespacePrefixPlaceholder.PowerShell.JsonUtilities
88
public static class JsonExtensions
99
{
1010
/// <summary>
11-
/// Recursively removes properties with the value "defaultnull" from a JSON structure
12-
/// and replaces string values that are "null" with actual null values.
13-
/// This method supports both JObject (JSON objects) and JArray (JSON arrays),
14-
/// ensuring proper cleanup of nested structures.
11+
/// Converts "null" strings to actual null values, replaces empty objects, and cleans up arrays.
1512
/// </summary>
16-
/// <param name="token">The JToken (JObject or JArray) to process.</param>
17-
/// <returns>The cleaned JSON string with "defaultnull" values removed and "null" strings converted to null.</returns>
18-
/// <example>
19-
/// JObject json = JObject.Parse(@"{""name"": ""John"", ""email"": ""defaultnull"", ""address"": ""null""}");
20-
/// string cleanedJson = json.RemoveDefaultNullProperties();
21-
/// Console.WriteLine(cleanedJson);
22-
/// // Output: { "name": "John", "address": null }
23-
/// </example>
24-
13+
/// <param name="token">The JSON token to process.</param>
14+
/// <returns>A cleaned JSON string with unnecessary null values removed.</returns>
2515
public static string RemoveDefaultNullProperties(this JToken token)
2616
{
2717
try
2818
{
2919
ProcessToken(token);
3020

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-
3521
return token.ToString();
3622
}
3723
catch (Exception)
@@ -44,15 +30,6 @@ private static JToken ProcessToken(JToken token)
4430
{
4531
if (token is JObject jsonObject)
4632
{
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-
}
5633

5734
// Recursively process remaining properties
5835
foreach (var property in jsonObject.Properties().ToList())
@@ -65,11 +42,12 @@ private static JToken ProcessToken(JToken token)
6542
property.Value = JValue.CreateNull();
6643
}
6744

68-
// Remove the property if it's now empty after processing
69-
if (ShouldRemove(cleanedValue))
45+
if (property.Value.ToString().Equals("{\r\n}", StringComparison.Ordinal))
7046
{
71-
property.Remove();
47+
48+
property.Value = JObject.Parse("{}"); // Convert empty object to {}
7249
}
50+
7351
}
7452

7553
// Remove the object itself if ALL properties are removed (empty object)
@@ -84,118 +62,34 @@ private static JToken ProcessToken(JToken token)
8462
// Process nested objects/arrays inside the array
8563
if (item is JObject || item is JArray)
8664
{
87-
JToken cleanedItem = ProcessToken(item);
88-
89-
if (ShouldRemove(cleanedItem))
65+
if (item.ToString().Equals("{\r\n}", StringComparison.Ordinal))
9066
{
91-
jsonArray.RemoveAt(i); // Remove empty or unnecessary items
67+
JToken cleanedItem = ProcessToken(item);
68+
jsonArray[i] = JObject.Parse("{}"); // Convert empty object to {}
9269
}
9370
else
9471
{
72+
JToken cleanedItem = ProcessToken(item);
9573
jsonArray[i] = cleanedItem; // Update with cleaned version
9674
}
75+
9776
}
9877
else if (item.Type == JTokenType.String && item.ToString().Equals("null", StringComparison.Ordinal))
9978
{
10079
jsonArray[i] = JValue.CreateNull(); // Convert "null" string to JSON null
10180
}
102-
else if (item.Type == JTokenType.String && item.ToString().Equals("defaultnull", StringComparison.Ordinal))
81+
else if (item.Type == JTokenType.String && item.ToString().Equals("nullarray", StringComparison.Ordinal))
10382
{
104-
jsonArray.RemoveAt(i); // Remove "defaultnull" entries
83+
jsonArray.RemoveAt(i);
84+
i--;
10585
}
86+
10687
}
10788

10889
return jsonArray.HasValues ? jsonArray : null;
10990
}
11091

11192
return token;
11293
}
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
156-
{
157-
JToken parsedValue = JToken.Parse(stringValue);
158-
property.Value = parsedValue; // Replace with unescaped JSON object
159-
ProcessBody(stringValue); // Recursively process
160-
}
161-
catch (Newtonsoft.Json.JsonException)
162-
{
163-
// If parsing fails, leave the value as is
164-
}
165-
}
166-
else if (value is JObject || value is JArray)
167-
{
168-
ProcessBody(value); // Recursively process nested objects/arrays
169-
}
170-
}
171-
}
172-
else if (token is JArray jsonArray)
173-
{
174-
for (int i = 0; i < jsonArray.Count; i++)
175-
{
176-
var value = jsonArray[i];
177-
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
183-
{
184-
JToken parsedValue = JToken.Parse(stringValue);
185-
jsonArray[i] = parsedValue; // Replace with unescaped JSON object
186-
ProcessBody(stringValue); // Recursively process
187-
}
188-
catch (Newtonsoft.Json.JsonException)
189-
{
190-
// If parsing fails, leave the value as is
191-
}
192-
}
193-
else if (value is JObject || value is JArray)
194-
{
195-
ProcessBody(value); // Recursively process nested objects/arrays
196-
}
197-
}
198-
}
199-
}
20094
}
20195
}

0 commit comments

Comments
 (0)