Skip to content

Commit ffaa365

Browse files
authored
fix(request-form): add handling for additional properties (#81)
This commit addresses an issue with the handling of additional properties in models that use [JsonExtensionData]. Specifically, it ensures that additional properties are flattened along with the model’s main members when data is sent as form data. Previously, the additional properties were being sent as a nested dictionary in form data, causing inconsistencies between the form data and the JSON body representation.
1 parent 3ab9d77 commit ffaa365

File tree

5 files changed

+193
-2
lines changed

5 files changed

+193
-2
lines changed

.editorconfig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ csharp_style_namespace_declarations = block_scoped:silent
196196
csharp_style_expression_bodied_lambdas = true:silent
197197
csharp_style_expression_bodied_local_functions = false:silent
198198

199+
[APIMatic.Core/Utilities/CoreHelper.cs]
200+
# Disables S3011 rule (Reflection should not be used to increase accessibility)
201+
dotnet_diagnostic.S3011.severity = none
202+
199203
[*.vb]
200204
###############################
201205
# VB Coding Conventions #
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Collections.Generic;
2+
using Newtonsoft.Json;
3+
4+
namespace APIMatic.Core.Test.MockTypes.Models
5+
{
6+
public class SimpleModelWithAdditionalPropertiesBaseModel : AdditionalPropertiesBaseModel
7+
{
8+
/// <summary>
9+
/// Initializes a new instance of the <see cref="SimpleModelWithAdditionalPropertiesBaseModel"/> class.
10+
/// </summary>
11+
/// <param name="requiredProperty">requiredProperty.</param>
12+
public SimpleModelWithAdditionalPropertiesBaseModel(
13+
string requiredProperty)
14+
{
15+
this.RequiredProperty = requiredProperty;
16+
}
17+
18+
/// <summary>
19+
/// The required property
20+
/// </summary>
21+
[JsonProperty("requiredProperty")]
22+
public string RequiredProperty { get; set; }
23+
}
24+
25+
/// <summary>
26+
/// BaseModel.
27+
/// </summary>
28+
public class AdditionalPropertiesBaseModel
29+
{
30+
/// <summary>
31+
/// Gets or sets a dictionary holding all the additional properties.
32+
/// </summary>
33+
[JsonExtensionData]
34+
public Dictionary<string, object> AdditionalProperties { get; set; }
35+
}
36+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
using System.Runtime.CompilerServices;
6+
using Newtonsoft.Json;
7+
using Newtonsoft.Json.Linq;
8+
9+
namespace APIMatic.Core.Test.MockTypes.Models
10+
{
11+
public class SimpleModelWithAdditionalPropertiesField
12+
{
13+
[JsonExtensionData]
14+
private readonly IDictionary<string, JToken> _additionalProperties;
15+
16+
/// <summary>
17+
/// Set the value associated with the specified key in the AdditionalProperties dictionary.
18+
/// </summary>
19+
[IndexerName("AdditionalPropertiesIndexer")]
20+
public string this[string key]
21+
{
22+
set => _additionalProperties.SetValue(key, value);
23+
}
24+
25+
/// <summary>
26+
/// Initializes a new instance of the <see cref="SimpleModelWithAdditionalPropertiesBaseModel"/> class.
27+
/// </summary>
28+
/// <param name="requiredProperty">requiredProperty.</param>
29+
public SimpleModelWithAdditionalPropertiesField(
30+
string requiredProperty)
31+
{
32+
this._additionalProperties = new Dictionary<string, JToken>();
33+
this.RequiredProperty = requiredProperty;
34+
}
35+
36+
/// <summary>
37+
/// The required property
38+
/// </summary>
39+
[JsonProperty("requiredProperty")]
40+
public string RequiredProperty { get; set; }
41+
}
42+
43+
internal static class AdditionalPropertiesExtensions
44+
{
45+
internal static void SetValue(this IDictionary<string, JToken> additionalProperties, string key, object value)
46+
{
47+
additionalProperties[key] = JToken.FromObject(value);
48+
}
49+
}
50+
}

APIMatic.Core.Test/Utilities/CoreHelperTest.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections;
33
using System.Collections.Generic;
44
using System.IO;
5+
using System.Linq;
56
using System.Text;
67
using APIMatic.Core.Http.Configuration;
78
using APIMatic.Core.Test.MockTypes.Models;
@@ -961,6 +962,59 @@ public void PrepareFormFieldsFromObject_IndexedCoreJsonValueParameter()
961962
List<KeyValuePair<string, object>> actual = CoreHelper.PrepareFormFieldsFromObject("jsonValue", jsonValue, ArraySerialization.Indexed);
962963
Assert.AreEqual(expected, actual);
963964
}
965+
966+
[Test]
967+
public void PrepareFormFieldsFromObject_WithAdditionalPropertiesAsField()
968+
{
969+
var queryBuilder = new StringBuilder();
970+
queryBuilder.Append(SERVER_URL);
971+
var simpleModelWithAdditionalPropertiesField =
972+
new SimpleModelWithAdditionalPropertiesField("Required Field")
973+
{
974+
["additionalPropertyKey"] = "additionalPropertyValue"
975+
};
976+
977+
List<KeyValuePair<string, object>> expected = new List<KeyValuePair<string, object>>()
978+
{
979+
new("simpleModelWithAdditionalPropertiesField[requiredProperty]", "Required Field"),
980+
new("simpleModelWithAdditionalPropertiesField[additionalPropertyKey]", "additionalPropertyValue")
981+
};
982+
983+
List<KeyValuePair<string, object>> actual = CoreHelper.PrepareFormFieldsFromObject(
984+
"simpleModelWithAdditionalPropertiesField", simpleModelWithAdditionalPropertiesField,
985+
ArraySerialization.Indexed);
986+
987+
Assert.AreEqual(expected, actual);
988+
}
989+
990+
[Test]
991+
public void PrepareFormFieldsFromObject_WithAdditionalPropertiesBaseModel()
992+
{
993+
var queryBuilder = new StringBuilder();
994+
queryBuilder.Append(SERVER_URL);
995+
var simpleModelWithAdditionalPropertiesBaseModel =
996+
new SimpleModelWithAdditionalPropertiesBaseModel("Required Field")
997+
{
998+
AdditionalProperties =
999+
new Dictionary<string, object> { { "additionalPropertyKey", "additionalPropertyValue" } }
1000+
};
1001+
1002+
List<KeyValuePair<string, object>> expected = new List<KeyValuePair<string, object>>()
1003+
{
1004+
new("simpleModelWithAdditionalPropertiesBaseModel[requiredProperty]", "Required Field"),
1005+
new("simpleModelWithAdditionalPropertiesBaseModel[additionalPropertyKey]", "additionalPropertyValue")
1006+
};
1007+
1008+
List<KeyValuePair<string, object>> actual = CoreHelper.PrepareFormFieldsFromObject(
1009+
"simpleModelWithAdditionalPropertiesBaseModel", simpleModelWithAdditionalPropertiesBaseModel,
1010+
ArraySerialization.Indexed);
1011+
1012+
// Assert that all items in 'expected' are found within 'actual'
1013+
bool containsAllExpectedEntries = expected.All(item => actual.Contains(item));
1014+
Assert.IsTrue(containsAllExpectedEntries, "Not all expected entries are present in actual.");
1015+
1016+
}
1017+
9641018
#endregion
9651019

9661020
#region DeepCloneObject

APIMatic.Core/Utilities/CoreHelper.cs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,24 +280,71 @@ private static string GetConvertedValue(object value, PropertyInfo propInfo)
280280
return convertedValue;
281281
}
282282

283-
private static void PrepareFormFieldsForCustomTypes(string name, object value, ArraySerialization arraySerializationFormat, List<KeyValuePair<string, object>> keys)
283+
private static void PrepareFormFieldsForCustomTypes(string name, object value,
284+
ArraySerialization arraySerializationFormat, List<KeyValuePair<string, object>> keys)
284285
{
285286
// Custom object Iterate through its properties
286287
var enumerator = value.GetType().GetProperties().GetEnumerator();
287288
var t = new JsonPropertyAttribute().GetType();
288289
while (enumerator.MoveNext())
289290
{
290291
var pInfo = enumerator.Current as PropertyInfo;
292+
if (pInfo?.GetIndexParameters().Length != 0) { continue; }
291293

292294
var jsonProperty = (JsonPropertyAttribute)pInfo.GetCustomAttributes(t, true).FirstOrDefault();
295+
293296
var subName = (jsonProperty != null) ? jsonProperty.PropertyName : pInfo.Name;
294297
string fullSubName = string.IsNullOrWhiteSpace(name) ? subName : name + '[' + subName + ']';
295298
var subValue = pInfo.GetValue(value, null);
296299
PrepareFormFieldsFromObject(fullSubName, subValue, arraySerializationFormat, keys, pInfo);
297300
}
301+
302+
ProcessAdditionalProperties(name, value, arraySerializationFormat, keys);
303+
}
304+
305+
private static void ProcessAdditionalProperties(
306+
string name, object value, ArraySerialization arraySerializationFormat,
307+
List<KeyValuePair<string, object>> keys)
308+
{
309+
// Find a member (field or property) with the [JsonExtensionData] attribute.
310+
var additionalPropertiesMember = value.GetType()
311+
.GetMembers(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
312+
.FirstOrDefault(member => member.GetCustomAttribute<JsonExtensionDataAttribute>() != null);
313+
314+
if (additionalPropertiesMember == null)
315+
{
316+
return;
317+
}
318+
319+
object additionalProperties = additionalPropertiesMember is FieldInfo fieldInfo
320+
? fieldInfo.GetValue(value)
321+
: (additionalPropertiesMember as PropertyInfo)?.GetValue(value);
322+
323+
switch (additionalProperties)
324+
{
325+
case IDictionary<string, JToken> additionalPropertiesJToken:
326+
HandleAdditionalProperties(additionalPropertiesJToken, name, arraySerializationFormat, keys);
327+
return;
328+
329+
case IDictionary<string, object> additionalPropertiesObj:
330+
HandleAdditionalProperties(additionalPropertiesObj, name, arraySerializationFormat, keys);
331+
return;
332+
}
333+
}
334+
335+
private static void HandleAdditionalProperties<T>(IDictionary<string, T> properties, string name,
336+
ArraySerialization arraySerializationFormat, List<KeyValuePair<string, object>> keys)
337+
{
338+
foreach (var kvp in properties)
339+
{
340+
string fullSubName = string.IsNullOrWhiteSpace(name) ? kvp.Key : $"{name}[{kvp.Key}]";
341+
PrepareFormFieldsFromObject(fullSubName, kvp.Value, arraySerializationFormat, keys, null);
342+
}
298343
}
299344

300-
private static void PrepareFormFieldsForDictionary(string name, IDictionary dictionary, ArraySerialization arraySerializationFormat, List<KeyValuePair<string, object>> keys = null, PropertyInfo propInfo = null)
345+
private static void PrepareFormFieldsForDictionary(string name, IDictionary dictionary,
346+
ArraySerialization arraySerializationFormat, List<KeyValuePair<string, object>> keys = null,
347+
PropertyInfo propInfo = null)
301348
{
302349
foreach (var sName in dictionary.Keys)
303350
{

0 commit comments

Comments
 (0)