Skip to content

Commit b7cf008

Browse files
authored
Respect PropertyNameCaseInsensitive setting (#42)
* Respect PropertyNameCaseInsensitive setting * Check in JsonPropertyNameAttribute also * Fix test * Respect PropertyNameCaseInsensitive setting in JsonObjects also
1 parent a7434e9 commit b7cf008

File tree

5 files changed

+126
-15
lines changed

5 files changed

+126
-15
lines changed

SystemTextJsonPatch.Tests/IntegrationTests/SimpleObjectIntegrationTest.cs

+50
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
22
using System.Dynamic;
3+
using System.Text.Json;
34
using SystemTextJsonPatch.Exceptions;
5+
using SystemTextJsonPatch.Operations;
46
using Xunit;
57

68
namespace SystemTextJsonPatch.IntegrationTests;
@@ -164,6 +166,54 @@ public void RegressionAspNetCore3634()
164166
Assert.Equal("For operation 'move', the target location specified by path '/Object/goodbye' was not found.", ex.Message);
165167
}
166168

169+
[Fact]
170+
public void ShouldFollowCaseInsensitiveSettingOfSystemTextJsonOptions()
171+
{
172+
var options = new JsonSerializerOptions()
173+
{
174+
PropertyNameCaseInsensitive = true
175+
};
176+
177+
// Arrange
178+
var targetObject = new SimpleObject()
179+
{
180+
StringProperty = "old",
181+
};
182+
183+
var patchDocument = new JsonPatchDocument<SimpleObject>();
184+
patchDocument.Operations.Add(new Operation<SimpleObject>("replace", "/stringproperty", null, "test"));
185+
patchDocument.Options = options;
186+
187+
// Act
188+
patchDocument.ApplyTo(targetObject);
189+
190+
// Assert
191+
Assert.Equal("test", targetObject.StringProperty);
192+
}
193+
194+
[Fact]
195+
public void FailsWhenPropertyCouldNotBeFoundBecauseOfCasing()
196+
{
197+
var options = new JsonSerializerOptions()
198+
{
199+
PropertyNameCaseInsensitive = false
200+
};
201+
202+
// Arrange
203+
var targetObject = new SimpleObject()
204+
{
205+
AnotherStringProperty = "old",
206+
};
207+
208+
var patchDocument = new JsonPatchDocument<SimpleObject>();
209+
patchDocument.Operations.Add(new Operation<SimpleObject>("replace", "/anotherstringproperty", null, "test"));
210+
patchDocument.Options = options;
211+
212+
// Act
213+
var excpetion = Assert.Throws<JsonPatchException>(() => patchDocument.ApplyTo(targetObject));
214+
Assert.Equal("The target location specified by path segment 'anotherstringproperty' was not found.", excpetion.Message);
215+
}
216+
167217
private class RegressionAspNetCore3634Object
168218
{
169219
public dynamic Object { get; set; }

SystemTextJsonPatch.Tests/JsonPatchDocumentJsonObjectTest.cs

+32
Original file line numberDiff line numberDiff line change
@@ -327,4 +327,36 @@ public void ApplyToModelReplaceNull()
327327
// Assert
328328
Assert.Null(model.CustomData["Email"]);
329329
}
330+
331+
[Fact]
332+
public void ApplyToModelReplaceWithIgnoringCasing()
333+
{
334+
// Arrange
335+
var model = new ObjectWithJsonNode { CustomData = JsonSerializer.SerializeToNode(new { Email = "[email protected]", Name = "Bar" }) };
336+
var patch = new JsonPatchDocument<ObjectWithJsonNode>();
337+
patch.Options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true };
338+
339+
patch.Operations.Add(new Operation<ObjectWithJsonNode>("replace", "/customdata/email", null, "[email protected]"));
340+
341+
// Act
342+
patch.ApplyTo(model);
343+
344+
// Assert
345+
Assert.Equal("[email protected]", model.CustomData["Email"].GetValue<string>());
346+
}
347+
348+
349+
[Fact]
350+
public void ApplyToModelReplaceWithInvalidCasing()
351+
{
352+
// Arrange
353+
var model = new ObjectWithJsonNode { CustomData = JsonSerializer.SerializeToNode(new { Email = "[email protected]", Name = "Bar" }) };
354+
var patch = new JsonPatchDocument<ObjectWithJsonNode>();
355+
356+
patch.Operations.Add(new Operation<ObjectWithJsonNode>("replace", "/CustomData/email", null, "[email protected]"));
357+
358+
// Assert
359+
var exception = Assert.Throws<JsonPatchException>(() => patch.ApplyTo(model));
360+
Assert.Equal("The target location specified by path segment 'email' was not found.", exception.Message);
361+
}
330362
}

SystemTextJsonPatch/Internal/JSonObjectAdapter.cs

+36-7
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ public bool TryAdd(object target, string segment, JsonSerializerOptions options,
1010
{
1111
var obj = (JsonObject)target;
1212

13-
obj[segment] = value != null ? JsonSerializer.SerializeToNode(value, options) : null;
13+
var propertyName = FindPropertyName(obj, segment, options);
14+
15+
obj[propertyName] = value != null ? JsonSerializer.SerializeToNode(value, options) : null;
1416

1517
errorMessage = null;
1618
return true;
@@ -20,7 +22,9 @@ public bool TryGet(object target, string segment, JsonSerializerOptions options,
2022
{
2123
var obj = (JsonObject)target;
2224

23-
if (!obj.TryGetPropertyValue(segment, out var valueAsToken))
25+
var propertyName = FindPropertyName(obj, segment, options);
26+
27+
if (!obj.TryGetPropertyValue(propertyName, out var valueAsToken))
2428
{
2529
value = null;
2630
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
@@ -36,7 +40,9 @@ public bool TryRemove(object target, string segment, JsonSerializerOptions optio
3640
{
3741
var obj = (JsonObject)target;
3842

39-
if (!obj.Remove(segment))
43+
var propertyName = FindPropertyName(obj, segment, options);
44+
45+
if (!obj.Remove(propertyName))
4046
{
4147
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
4248
return false;
@@ -50,13 +56,15 @@ public bool TryReplace(object target, string segment, JsonSerializerOptions opti
5056
{
5157
var obj = (JsonObject)target;
5258

53-
if (obj[segment] == null)
59+
var propertyName = FindPropertyName(obj, segment, options);
60+
61+
if (obj[propertyName] == null)
5462
{
5563
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
5664
return false;
5765
}
5866

59-
obj[segment] = value != null ? JsonSerializer.SerializeToNode(value, options) : null;
67+
obj[propertyName] = value != null ? JsonSerializer.SerializeToNode(value, options) : null;
6068

6169
errorMessage = null;
6270
return true;
@@ -66,7 +74,9 @@ public bool TryTest(object target, string segment, JsonSerializerOptions options
6674
{
6775
var obj = (JsonObject)target;
6876

69-
if (!obj.TryGetPropertyValue(segment, out var currentValue))
77+
var propertyName = FindPropertyName(obj, segment, options);
78+
79+
if (!obj.TryGetPropertyValue(propertyName, out var currentValue))
7080
{
7181
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
7282
return false;
@@ -95,7 +105,9 @@ public bool TryTraverse(object target, string segment, JsonSerializerOptions opt
95105
{
96106
var obj = (JsonObject)target;
97107

98-
if (!obj.TryGetPropertyValue(segment, out JsonNode? nextTargetToken))
108+
var propertyName = FindPropertyName(obj, segment, options);
109+
110+
if (!obj.TryGetPropertyValue(propertyName, out JsonNode? nextTargetToken))
99111
{
100112
nextTarget = null;
101113
errorMessage = null;
@@ -106,4 +118,21 @@ public bool TryTraverse(object target, string segment, JsonSerializerOptions opt
106118
errorMessage = null;
107119
return true;
108120
}
121+
122+
private static string FindPropertyName(JsonObject? obj, string segment, JsonSerializerOptions options)
123+
{
124+
if (!options.PropertyNameCaseInsensitive || obj == null)
125+
return segment;
126+
127+
if (obj.ContainsKey(segment))
128+
return segment;
129+
130+
foreach (var node in obj)
131+
{
132+
if (string.Equals(node.Key, segment, StringComparison.OrdinalIgnoreCase))
133+
return node.Key;
134+
}
135+
136+
return segment;
137+
}
109138
}

SystemTextJsonPatch/Internal/PocoAdapter.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,6 @@ private static bool TryGetJsonProperty(object target, string segment, JsonSerial
224224
return new JsonNodeProxy(jsonElement, propertyName);
225225
}
226226

227-
228-
return PropertyProxyCache.GetPropertyProxy(target.GetType(), propertyName, options.PropertyNamingPolicy);
227+
return PropertyProxyCache.GetPropertyProxy(target.GetType(), propertyName, options.PropertyNamingPolicy, options.PropertyNameCaseInsensitive);
229228
}
230229
}

SystemTextJsonPatch/Internal/PropertyProxyCache.cs

+7-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using System.Reflection;
44
using System.Text.Json;
55
using System.Text.Json.Serialization;
6-
using System.Xml.Linq;
76
using SystemTextJsonPatch.Exceptions;
87
using SystemTextJsonPatch.Internal.Proxies;
98

@@ -15,7 +14,7 @@ internal static class PropertyProxyCache
1514
// Naming policy has to be part of the key because it can change the target property
1615
private static readonly ConcurrentDictionary<(Type, string, JsonNamingPolicy?), PropertyProxy?> CachedPropertyProxies = new();
1716

18-
internal static PropertyProxy? GetPropertyProxy(Type type, string propName, JsonNamingPolicy? namingPolicy)
17+
internal static PropertyProxy? GetPropertyProxy(Type type, string propName, JsonNamingPolicy? namingPolicy, bool? propertyNameCaseInsensitive)
1918
{
2019
var key = (type, propName, namingPolicy);
2120

@@ -30,19 +29,21 @@ internal static class PropertyProxyCache
3029
CachedTypeProperties[type] = properties;
3130
}
3231

33-
propertyProxy = FindPropertyInfo(properties, propName, namingPolicy);
32+
propertyProxy = FindPropertyInfo(properties, propName, namingPolicy, propertyNameCaseInsensitive);
3433
CachedPropertyProxies[key] = propertyProxy;
3534

3635
return propertyProxy;
3736
}
3837

39-
private static PropertyProxy? FindPropertyInfo(PropertyInfo[] properties, string propName, JsonNamingPolicy? namingPolicy)
38+
private static PropertyProxy? FindPropertyInfo(PropertyInfo[] properties, string propName, JsonNamingPolicy? namingPolicy, bool? propertyNameCaseInsensitive)
4039
{
40+
var comparison = propertyNameCaseInsensitive == true ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
41+
4142
// First check through all properties if property name matches JsonPropertyNameAttribute
4243
foreach (var propertyInfo in properties)
4344
{
4445
var jsonPropertyNameAttr = propertyInfo.GetCustomAttribute<JsonPropertyNameAttribute>();
45-
if (jsonPropertyNameAttr != null && string.Equals(jsonPropertyNameAttr.Name, propName, StringComparison.Ordinal))
46+
if (jsonPropertyNameAttr != null && string.Equals(jsonPropertyNameAttr.Name, propName, comparison))
4647
{
4748
EnsureAccessToProperty(propertyInfo);
4849
return new PropertyProxy(propertyInfo);
@@ -54,7 +55,7 @@ internal static class PropertyProxyCache
5455
{
5556
var propertyName = namingPolicy != null ? namingPolicy.ConvertName(propertyInfo.Name) : propertyInfo.Name;
5657

57-
if (string.Equals(propertyName, propName, StringComparison.Ordinal))
58+
if (string.Equals(propertyName, propName, comparison))
5859
{
5960
EnsureAccessToProperty(propertyInfo);
6061
return new PropertyProxy(propertyInfo);

0 commit comments

Comments
 (0)