Skip to content

Commit

Permalink
breaking change: Changed property matching to follow more closely Sys…
Browse files Browse the repository at this point in the history
…tem.Text.Json naming policy. Fixes #36
  • Loading branch information
Havunen committed Oct 18, 2024
1 parent a888c60 commit 3554f1c
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using SystemTextJsonPatch.Exceptions;
using System.Text.Json;
using SystemTextJsonPatch.Exceptions;
using Xunit;

namespace SystemTextJsonPatch.IntegrationTests;
Expand Down Expand Up @@ -32,6 +33,7 @@ public void AddNewPropertyToNestedAnonymousObjectShouldFail()
};

var patchDocument = new JsonPatchDocument();
patchDocument.Options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
patchDocument.Add("Nested/NewInt", 1);

// Act
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public void JsonExceptionsFromCustomConvertersShouldBeShownAsIs()
{
var serializerOptions = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters =
{
new CustomJsonConverter()
Expand All @@ -56,6 +57,7 @@ public void JsonExceptionsFromSystemTextJsonSerializerShouldNotBeShown()
{
var serializerOptions = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

var model = new TestModel();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public void TestValuesShouldBeEqualRegardlessOfNumberOfDecimalZeroes()
};
var jsonOptions = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

var incomingJson = JsonSerializer.Serialize(incomingOperations, jsonOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public void JsonPatchHasErrorNotUsedInOperationDateTime()

var jsonOptions = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

var incomingJson = JsonSerializer.Serialize(incomingOperations, jsonOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ public void ReplaceFullGenericListWithCollection()
}
};

var json = "[{\"op\":\"replace\",\"path\":\"/simpleobjectList\",\"value\":[{\"AnotherIntegerValue\": 6}]}]";
var json = "[{\"op\":\"replace\",\"path\":\"/SimpleObjectList\",\"value\":[{\"AnotherIntegerValue\": 6}]}]";
var docJson = JsonSerializer.Deserialize<JsonPatchDocument<SimpleObjectWithNestedObject>>(json);


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public void ReplaceNestedObjectWithSerialization()
#if NET8_0

[Fact]
public void ReplaceNestedObjectWithPlainStrings()
public void SnakeCaseOpMatchesProperty()
{
// Arrange
var targetObject = new SimpleObjectWithNestedObject()
Expand All @@ -76,7 +76,7 @@ public void ReplaceNestedObjectWithPlainStrings()

var newNested = new NestedObject { StringProperty = "B" };
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
patchDocument.Operations.Add(new Operation<SimpleObjectWithNestedObject>("replace", "/nested_object", JsonSerializer.Serialize(newNested, options)));
patchDocument.Operations.Add(new Operation<SimpleObjectWithNestedObject>("replace", "/nested_object", null, newNested));
patchDocument.Options = options;

var serialized = JsonSerializer.Serialize(patchDocument, options);
Expand All @@ -88,9 +88,103 @@ public void ReplaceNestedObjectWithPlainStrings()
// Assert
Assert.Equal("B", targetObject.NestedObject.StringProperty);
}

#endif

[Fact]
public void CamelCaseOpMatchesExactProperty()
{
// Arrange
var targetObject = new NameCasingTestObject()
{
PropertyName = 1, // This is before propertyName and camelCase matches it so it is used.
propertyName = 1
};
var options = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

var patchDocument = new JsonPatchDocument<NameCasingTestObject>();
patchDocument.Operations.Add(new Operation<NameCasingTestObject>("replace", "/propertyName", null, 2));
patchDocument.Options = options;

var serialized = JsonSerializer.Serialize(patchDocument, options);
var deserialized = JsonSerializer.Deserialize<JsonPatchDocument<NameCasingTestObject>>(serialized, options);

// Act
deserialized.ApplyTo(targetObject);

// Assert
Assert.Equal(2, targetObject.PropertyName);
Assert.Equal(1, targetObject.propertyName);
}

[Fact]
public void MatchesExactProperty()
{
// Arrange
var targetObject = new NameCasingTestObject()
{
PropertyName = 1,
propertyName = 1 // exact match
};
var options = new JsonSerializerOptions()
{
};

var patchDocument = new JsonPatchDocument<NameCasingTestObject>();
patchDocument.Operations.Add(new Operation<NameCasingTestObject>("replace", "/propertyName", null, 2));
patchDocument.Options = options;

var serialized = JsonSerializer.Serialize(patchDocument, options);
var deserialized = JsonSerializer.Deserialize<JsonPatchDocument<NameCasingTestObject>>(serialized, options);

// Act
deserialized.ApplyTo(targetObject);

// Assert
Assert.Equal(1, targetObject.PropertyName);
Assert.Equal(2, targetObject.propertyName);
}

[Fact]
public void MatchesExactPropertyTestCache()
{
CamelCaseOpMatchesExactPropertyAttributeOverride();
CamelCaseOpMatchesExactProperty();
MatchesExactProperty();
}

[Fact]
public void CamelCaseOpMatchesExactPropertyAttributeOverride()
{
// Arrange
var targetObject = new AttrNameCasingTestObject()
{
PropertyName = 1,
propertyName = 1
};
var options = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

var patchDocument = new JsonPatchDocument<AttrNameCasingTestObject>();
patchDocument.Operations.Add(new Operation<AttrNameCasingTestObject>("replace", "/propertyName", null, 2));
patchDocument.Options = options;

var serialized = JsonSerializer.Serialize(patchDocument, options);
var deserialized = JsonSerializer.Deserialize<JsonPatchDocument<AttrNameCasingTestObject>>(serialized, options);

// Act
deserialized.ApplyTo(targetObject);

// Assert
Assert.Equal(2, targetObject.PropertyName); // attribute takes precedence
Assert.Equal(1, targetObject.propertyName);
}

[Fact]
public void TestStringPropertyInNestedObject()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json.Nodes;
using System.Text.Json;
using System.Text.Json.Nodes;
using SystemTextJsonPatch.Operations;
using Xunit;

Expand All @@ -10,6 +11,7 @@ public class TestOperationIntegrationTests
public void EmptyStringTest_Dto()
{
var patchDocument = new JsonPatchDocument<TestClass>();
patchDocument.Options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
patchDocument.Operations.Add(new Operation<TestClass>(op: "test", path: "/string", from: null, value: ""));
var node = new TestClass() { String = "" };

Expand All @@ -20,6 +22,7 @@ public void EmptyStringTest_Dto()
public void NullFieldTest_Dto()
{
var patchDocument = new JsonPatchDocument<TestClass>();
patchDocument.Options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
patchDocument.Operations.Add(new Operation<TestClass>(op: "test", path: "/string", from: null, value: null));
var node = new TestClass() { String = null };

Expand All @@ -30,6 +33,7 @@ public void NullFieldTest_Dto()
public void EmptyStringTest_Json()
{
var patchDocument = new JsonPatchDocument<JsonNode>();
patchDocument.Options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
patchDocument.Operations.Add(new Operation<JsonNode>(op: "test", path: "/string", from: null, value: ""));
var node = JsonNode.Parse("{\"string\": \"\"}")!;
patchDocument.ApplyTo(node);
Expand All @@ -39,6 +43,7 @@ public void EmptyStringTest_Json()
public void NullFieldTest_Json()
{
var patchDocument = new JsonPatchDocument<JsonNode>();
patchDocument.Options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
patchDocument.Operations.Add(new Operation<JsonNode>(op: "test", path: "/string", from: null, value: null));
var node = JsonNode.Parse("{\"string\": null}")!;
patchDocument.ApplyTo(node);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace SystemTextJsonPatch;

public class NameCasingTestObject
{
public int PropertyName { get; set; }

public int propertyName { get; set; }

}

public class AttrNameCasingTestObject
{
[JsonPropertyName("propertyName")]
public int PropertyName { get; set; }

public int propertyName { get; set; }

}
2 changes: 1 addition & 1 deletion SystemTextJsonPatch/Internal/PocoAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,6 @@ private static bool TryGetJsonProperty(object target, string segment, JsonSerial
}


return PropertyProxyCache.GetPropertyProxy(target.GetType(), propertyName);
return PropertyProxyCache.GetPropertyProxy(target.GetType(), propertyName, options.PropertyNamingPolicy);
}
}
19 changes: 12 additions & 7 deletions SystemTextJsonPatch/Internal/PropertyProxyCache.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections.Concurrent;
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 @@ -10,11 +12,12 @@ namespace SystemTextJsonPatch.Internal
internal static class PropertyProxyCache
{
private static readonly ConcurrentDictionary<Type, PropertyInfo[]> CachedTypeProperties = new();
private static readonly ConcurrentDictionary<(Type, string), PropertyProxy?> CachedPropertyProxies = new();
// 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)
internal static PropertyProxy? GetPropertyProxy(Type type, string propName, JsonNamingPolicy? namingPolicy)
{
var key = (type, propName);
var key = (type, propName, namingPolicy);

if (CachedPropertyProxies.TryGetValue(key, out var propertyProxy))
{
Expand All @@ -27,19 +30,19 @@ internal static class PropertyProxyCache
CachedTypeProperties[type] = properties;
}

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

return propertyProxy;
}

private static PropertyProxy? FindPropertyInfo(PropertyInfo[] properties, string propName)
private static PropertyProxy? FindPropertyInfo(PropertyInfo[] properties, string propName, JsonNamingPolicy? namingPolicy)
{
// 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.OrdinalIgnoreCase))
if (jsonPropertyNameAttr != null && string.Equals(jsonPropertyNameAttr.Name, propName, StringComparison.Ordinal))
{
EnsureAccessToProperty(propertyInfo);
return new PropertyProxy(propertyInfo);
Expand All @@ -49,7 +52,9 @@ internal static class PropertyProxyCache
// If it didn't find match by JsonPropertyName then use property name
foreach (var propertyInfo in properties)
{
if (string.Equals(propertyInfo.Name, propName, StringComparison.OrdinalIgnoreCase))
var propertyName = namingPolicy != null ? namingPolicy.ConvertName(propertyInfo.Name) : propertyInfo.Name;

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

0 comments on commit 3554f1c

Please sign in to comment.