Skip to content

Fixes removal of boolean and empty Json object values from request body #3224

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Teams/beta/custom/MicrosoftGraphRscConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ public partial class MicrosoftGraphRscConfiguration :
IMicrosoftGraphRscConfigurationInternal,
Runtime.IValidates
{
private readonly PropertyTracker _propertyTracker = new PropertyTracker();
public void TrackProperty(string propertyName) => _propertyTracker.TrackProperty(propertyName);
public bool IsPropertySet(string propertyName) =>_propertyTracker.IsPropertySet(propertyName);
public T SanitizeValue<T>(object value) => PropertyTracker.SanitizeValue<T>(value);
/// <summary>
/// Backing field for Inherited model <see cref= "Microsoft.Graph.Beta.PowerShell.Models.IMicrosoftGraphEntity" />
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Teams/beta/custom/MicrosoftGraphTeamsAppPreApproval.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ public partial class MicrosoftGraphTeamsAppPreApproval :
IMicrosoftGraphTeamsAppPreApprovalInternal,
Runtime.IValidates
{
private readonly PropertyTracker _propertyTracker = new PropertyTracker();
public void TrackProperty(string propertyName) => _propertyTracker.TrackProperty(propertyName);
public bool IsPropertySet(string propertyName) =>_propertyTracker.IsPropertySet(propertyName);
public T SanitizeValue<T>(object value) => PropertyTracker.SanitizeValue<T>(value);

/// <summary>
/// Backing field for Inherited model <see cref= "Microsoft.Graph.Beta.PowerShell.Models.IMicrosoftGraphEntity" />
/// </summary>
Expand Down
49 changes: 44 additions & 5 deletions src/readme.graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ directive:
- from: source-file-csharp
where: $
transform: >
if (!$documentPath.match(/generated%2Fapi%2FModels%2F\w*MicrosoftGraph\w*\d*.json.cs/gm))
if (!$documentPath.match(/generated%2Fapi%2FModels%2F\w*\d*.json.cs/gm))
{
return $;
} else {
Expand Down Expand Up @@ -333,11 +333,26 @@ directive:
// Ensure dateTime is always serialized as Utc.
let dateTimeToJsonRegex = /(\.Json\.JsonString\()(.*)\?(\.ToString\(@"yyyy'-'MM'-'dd'T'HH':'mm':'ss\.fffffffK")/gm
$ = $.replace(dateTimeToJsonRegex, '$1System.DateTime.SpecifyKind($2.Value.ToUniversalTime(), System.DateTimeKind.Utc)$3');

//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.

// Enables null valued properties
$ = $.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")')
const regexP = /AddIf\(\s*null\s*!=\s*\(\(\(object\)this\._(\w+).*?(\(Microsoft.*.PowerShell\.Runtime\.Json\.JsonNode\)).*?"(\w+)".*?container\.Add\s*\);/gm
$ = $.replace(regexP, (match, p1, p2, p3) => {
let capitalizedP1 = p1.charAt(0).toUpperCase() + p1.slice(1); // Capitalize first letter
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}`;
});

$ = $.replace(/if\s*\(\s*null\s*!=\s*this\._(\w+)\s*\)/gm, 'if(this.IsPropertySet("$1"))')

let nameSpacePrefixRegex = /(Microsoft(?:\.\w+)*?\.PowerShell)/gm
let nameSpacePrefix = 'Microsoft.Graph.PowerShell';
if($.match(nameSpacePrefixRegex)){
let prefixMatch = nameSpacePrefixRegex.exec($);
nameSpacePrefix = prefixMatch[1];
}
$ = $.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);');

$ = $.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")');
$ =$.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}');

return $;
}
Expand Down Expand Up @@ -395,7 +410,7 @@ directive:
- from: source-file-csharp
where: $
transform: >
if (!$documentPath.match(/generated%2Fapi%2FModels%2F\w*MicrosoftGraph\w*\d*.cs/gm))
if (!$documentPath.match(/generated%2Fapi%2FModels%2F\w*\d*.cs/gm))
{
return $;
} else {
Expand All @@ -404,8 +419,31 @@ directive:
if($.match(additionalPropertiesRegex)) {
$ = $.replace(additionalPropertiesRegex, '$1$2 new $3');
}
//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.
$ = $.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}')

$ = $.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}')

$ = $.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}')

const match = $documentPath.match(/generated%2Fapi%2FModels%2F([\w]*[\w\d]*)\.cs/gm);
if (match) {
let fileName = match[0];
fileName = fileName.replace('generated%2Fapi%2FModels%2F','')
fileName = fileName.replace('.cs','')
const interfaceName = 'I'+fileName
$ = $.replace('interface '+interfaceName+' :', 'interface '+interfaceName+' : IPropertyTracker,')
const className = fileName
const regexP = new RegExp(`public\\s+partial\\s+class\\s+${className}\\s*:\\s*[\\s\\S]*?{`, "gm");
var matches = regexP.exec($);
let originalMatch = matches[0];
$ = $.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);');
}

$ = $.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}')

return $;

}
# Modify generated .cs cmdlets.
- from: source-file-csharp
Expand Down Expand Up @@ -658,6 +696,7 @@ directive:

$ = $.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);');

$ = $.replace(/cleanedBody = Microsoft.*.ReplaceAndRemoveSlashes\(cleanedBody\);/gm,'')
return $
}

Expand Down
9 changes: 9 additions & 0 deletions tools/Custom/IPropertyTracker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace NamespacePrefixPlaceholder.PowerShell.Models
{
public interface IPropertyTracker
{
void TrackProperty(string propertyName);
bool IsPropertySet(string propertyName);
T SanitizeValue<T>(object value);
}
}
138 changes: 16 additions & 122 deletions tools/Custom/JsonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,16 @@ namespace NamespacePrefixPlaceholder.PowerShell.JsonUtilities
public static class JsonExtensions
{
/// <summary>
/// Recursively removes properties with the value "defaultnull" from a JSON structure
/// and replaces string values that are "null" with actual null values.
/// This method supports both JObject (JSON objects) and JArray (JSON arrays),
/// ensuring proper cleanup of nested structures.
/// Converts "null" strings to actual null values, replaces empty objects, and cleans up arrays.
/// </summary>
/// <param name="token">The JToken (JObject or JArray) to process.</param>
/// <returns>The cleaned JSON string with "defaultnull" values removed and "null" strings converted to null.</returns>
/// <example>
/// JObject json = JObject.Parse(@"{""name"": ""John"", ""email"": ""defaultnull"", ""address"": ""null""}");
/// string cleanedJson = json.RemoveDefaultNullProperties();
/// Console.WriteLine(cleanedJson);
/// // Output: { "name": "John", "address": null }
/// </example>

/// <param name="token">The JSON token to process.</param>
/// <returns>A cleaned JSON string with unnecessary null values removed.</returns>
public static string RemoveDefaultNullProperties(this JToken token)
{
try
{
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)
Expand All @@ -44,15 +30,6 @@ 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())
Expand All @@ -65,11 +42,12 @@ private static JToken ProcessToken(JToken token)
property.Value = JValue.CreateNull();
}

// Remove the property if it's now empty after processing
if (ShouldRemove(cleanedValue))
if (property.Value.ToString().Equals("{\r\n}", StringComparison.Ordinal))
{
property.Remove();

property.Value = JObject.Parse("{}"); // Convert empty object to {}
}

}

// Remove the object itself if ALL properties are removed (empty object)
Expand All @@ -84,118 +62,34 @@ private static JToken ProcessToken(JToken token)
// Process nested objects/arrays inside the array
if (item is JObject || item is JArray)
{
JToken cleanedItem = ProcessToken(item);

if (ShouldRemove(cleanedItem))
if (item.ToString().Equals("{\r\n}", StringComparison.Ordinal))
{
jsonArray.RemoveAt(i); // Remove empty or unnecessary items
JToken cleanedItem = ProcessToken(item);
jsonArray[i] = JObject.Parse("{}"); // Convert empty object to {}
}
else
{
JToken cleanedItem = ProcessToken(item);
jsonArray[i] = cleanedItem; // Update with cleaned version
}

}
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))
else if (item.Type == JTokenType.String && item.ToString().Equals("nullarray", StringComparison.Ordinal))
{
jsonArray.RemoveAt(i); // Remove "defaultnull" entries
jsonArray.RemoveAt(i);
i--;
}

}

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
{
JToken parsedValue = JToken.Parse(stringValue);
property.Value = parsedValue; // Replace with unescaped JSON object
ProcessBody(stringValue); // Recursively process
}
catch (Newtonsoft.Json.JsonException)
{
// 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)
{
for (int i = 0; i < jsonArray.Count; i++)
{
var value = jsonArray[i];

// 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
{
JToken parsedValue = JToken.Parse(stringValue);
jsonArray[i] = parsedValue; // Replace with unescaped JSON object
ProcessBody(stringValue); // Recursively process
}
catch (Newtonsoft.Json.JsonException)
{
// If parsing fails, leave the value as is
}
}
else if (value is JObject || value is JArray)
{
ProcessBody(value); // Recursively process nested objects/arrays
}
}
}
}
}
}
Loading
Loading