Skip to content

Commit

Permalink
Respect PropertyNameCaseInsensitive setting (#42)
Browse files Browse the repository at this point in the history
* Respect PropertyNameCaseInsensitive setting

* Check in JsonPropertyNameAttribute also

* Fix test

* Respect PropertyNameCaseInsensitive setting in JsonObjects also
  • Loading branch information
twenzel authored Jan 16, 2025
1 parent a7434e9 commit b7cf008
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Dynamic;
using System.Text.Json;
using SystemTextJsonPatch.Exceptions;
using SystemTextJsonPatch.Operations;
using Xunit;

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

[Fact]
public void ShouldFollowCaseInsensitiveSettingOfSystemTextJsonOptions()
{
var options = new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
};

// Arrange
var targetObject = new SimpleObject()
{
StringProperty = "old",
};

var patchDocument = new JsonPatchDocument<SimpleObject>();
patchDocument.Operations.Add(new Operation<SimpleObject>("replace", "/stringproperty", null, "test"));
patchDocument.Options = options;

// Act
patchDocument.ApplyTo(targetObject);

// Assert
Assert.Equal("test", targetObject.StringProperty);
}

[Fact]
public void FailsWhenPropertyCouldNotBeFoundBecauseOfCasing()
{
var options = new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = false
};

// Arrange
var targetObject = new SimpleObject()
{
AnotherStringProperty = "old",
};

var patchDocument = new JsonPatchDocument<SimpleObject>();
patchDocument.Operations.Add(new Operation<SimpleObject>("replace", "/anotherstringproperty", null, "test"));
patchDocument.Options = options;

// Act
var excpetion = Assert.Throws<JsonPatchException>(() => patchDocument.ApplyTo(targetObject));
Assert.Equal("The target location specified by path segment 'anotherstringproperty' was not found.", excpetion.Message);
}

private class RegressionAspNetCore3634Object
{
public dynamic Object { get; set; }
Expand Down
32 changes: 32 additions & 0 deletions SystemTextJsonPatch.Tests/JsonPatchDocumentJsonObjectTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -327,4 +327,36 @@ public void ApplyToModelReplaceNull()
// Assert
Assert.Null(model.CustomData["Email"]);
}

[Fact]
public void ApplyToModelReplaceWithIgnoringCasing()
{
// Arrange
var model = new ObjectWithJsonNode { CustomData = JsonSerializer.SerializeToNode(new { Email = "foo@bar.com", Name = "Bar" }) };
var patch = new JsonPatchDocument<ObjectWithJsonNode>();
patch.Options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true };

patch.Operations.Add(new Operation<ObjectWithJsonNode>("replace", "/customdata/email", null, "foo@baz.com"));

// Act
patch.ApplyTo(model);

// Assert
Assert.Equal("foo@baz.com", model.CustomData["Email"].GetValue<string>());
}


[Fact]
public void ApplyToModelReplaceWithInvalidCasing()
{
// Arrange
var model = new ObjectWithJsonNode { CustomData = JsonSerializer.SerializeToNode(new { Email = "foo@bar.com", Name = "Bar" }) };
var patch = new JsonPatchDocument<ObjectWithJsonNode>();

patch.Operations.Add(new Operation<ObjectWithJsonNode>("replace", "/CustomData/email", null, "foo@baz.com"));

// Assert
var exception = Assert.Throws<JsonPatchException>(() => patch.ApplyTo(model));
Assert.Equal("The target location specified by path segment 'email' was not found.", exception.Message);
}
}
43 changes: 36 additions & 7 deletions SystemTextJsonPatch/Internal/JSonObjectAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ public bool TryAdd(object target, string segment, JsonSerializerOptions options,
{
var obj = (JsonObject)target;

obj[segment] = value != null ? JsonSerializer.SerializeToNode(value, options) : null;
var propertyName = FindPropertyName(obj, segment, options);

obj[propertyName] = value != null ? JsonSerializer.SerializeToNode(value, options) : null;

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

if (!obj.TryGetPropertyValue(segment, out var valueAsToken))
var propertyName = FindPropertyName(obj, segment, options);

if (!obj.TryGetPropertyValue(propertyName, out var valueAsToken))
{
value = null;
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
Expand All @@ -36,7 +40,9 @@ public bool TryRemove(object target, string segment, JsonSerializerOptions optio
{
var obj = (JsonObject)target;

if (!obj.Remove(segment))
var propertyName = FindPropertyName(obj, segment, options);

if (!obj.Remove(propertyName))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
Expand All @@ -50,13 +56,15 @@ public bool TryReplace(object target, string segment, JsonSerializerOptions opti
{
var obj = (JsonObject)target;

if (obj[segment] == null)
var propertyName = FindPropertyName(obj, segment, options);

if (obj[propertyName] == null)
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}

obj[segment] = value != null ? JsonSerializer.SerializeToNode(value, options) : null;
obj[propertyName] = value != null ? JsonSerializer.SerializeToNode(value, options) : null;

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

if (!obj.TryGetPropertyValue(segment, out var currentValue))
var propertyName = FindPropertyName(obj, segment, options);

if (!obj.TryGetPropertyValue(propertyName, out var currentValue))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
Expand Down Expand Up @@ -95,7 +105,9 @@ public bool TryTraverse(object target, string segment, JsonSerializerOptions opt
{
var obj = (JsonObject)target;

if (!obj.TryGetPropertyValue(segment, out JsonNode? nextTargetToken))
var propertyName = FindPropertyName(obj, segment, options);

if (!obj.TryGetPropertyValue(propertyName, out JsonNode? nextTargetToken))
{
nextTarget = null;
errorMessage = null;
Expand All @@ -106,4 +118,21 @@ public bool TryTraverse(object target, string segment, JsonSerializerOptions opt
errorMessage = null;
return true;
}

private static string FindPropertyName(JsonObject? obj, string segment, JsonSerializerOptions options)
{
if (!options.PropertyNameCaseInsensitive || obj == null)
return segment;

if (obj.ContainsKey(segment))
return segment;

foreach (var node in obj)
{
if (string.Equals(node.Key, segment, StringComparison.OrdinalIgnoreCase))
return node.Key;
}

return segment;
}
}
3 changes: 1 addition & 2 deletions SystemTextJsonPatch/Internal/PocoAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,6 @@ private static bool TryGetJsonProperty(object target, string segment, JsonSerial
return new JsonNodeProxy(jsonElement, propertyName);
}


return PropertyProxyCache.GetPropertyProxy(target.GetType(), propertyName, options.PropertyNamingPolicy);
return PropertyProxyCache.GetPropertyProxy(target.GetType(), propertyName, options.PropertyNamingPolicy, options.PropertyNameCaseInsensitive);
}
}
13 changes: 7 additions & 6 deletions SystemTextJsonPatch/Internal/PropertyProxyCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Xml.Linq;
using SystemTextJsonPatch.Exceptions;
using SystemTextJsonPatch.Internal.Proxies;

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

internal static PropertyProxy? GetPropertyProxy(Type type, string propName, JsonNamingPolicy? namingPolicy)
internal static PropertyProxy? GetPropertyProxy(Type type, string propName, JsonNamingPolicy? namingPolicy, bool? propertyNameCaseInsensitive)
{
var key = (type, propName, namingPolicy);

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

propertyProxy = FindPropertyInfo(properties, propName, namingPolicy);
propertyProxy = FindPropertyInfo(properties, propName, namingPolicy, propertyNameCaseInsensitive);
CachedPropertyProxies[key] = propertyProxy;

return propertyProxy;
}

private static PropertyProxy? FindPropertyInfo(PropertyInfo[] properties, string propName, JsonNamingPolicy? namingPolicy)
private static PropertyProxy? FindPropertyInfo(PropertyInfo[] properties, string propName, JsonNamingPolicy? namingPolicy, bool? propertyNameCaseInsensitive)
{
var comparison = propertyNameCaseInsensitive == true ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;

// First check through all properties if property name matches JsonPropertyNameAttribute
foreach (var propertyInfo in properties)
{
var jsonPropertyNameAttr = propertyInfo.GetCustomAttribute<JsonPropertyNameAttribute>();
if (jsonPropertyNameAttr != null && string.Equals(jsonPropertyNameAttr.Name, propName, StringComparison.Ordinal))
if (jsonPropertyNameAttr != null && string.Equals(jsonPropertyNameAttr.Name, propName, comparison))
{
EnsureAccessToProperty(propertyInfo);
return new PropertyProxy(propertyInfo);
Expand All @@ -54,7 +55,7 @@ internal static class PropertyProxyCache
{
var propertyName = namingPolicy != null ? namingPolicy.ConvertName(propertyInfo.Name) : propertyInfo.Name;

if (string.Equals(propertyName, propName, StringComparison.Ordinal))
if (string.Equals(propertyName, propName, comparison))
{
EnsureAccessToProperty(propertyInfo);
return new PropertyProxy(propertyInfo);
Expand Down

0 comments on commit b7cf008

Please sign in to comment.