Skip to content

Commit 62deb28

Browse files
authored
Add property filter and fix null property diff bug (#30)
* Add property filter * Force diff in array diff when property filter set * Fix diff for nulls * Fix diff context and add test * Add release notes for 1.3.1
1 parent cb853ff commit 62deb28

File tree

10 files changed

+233
-37
lines changed

10 files changed

+233
-37
lines changed

ReleaseNotes.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Release Notes
22

3+
## 1.3.1
4+
5+
- Added `PropertyFilter` to `JsonDiffOptions` (#29)
6+
- Fixed bug in diffing null-valued properties (#31)
7+
38
## 1.3.0
49

510
- **Added `DeepEquals` implementation for `JsonDocument` and `JsonElement`**

src/Directory.Build.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</PropertyGroup>
1010

1111
<PropertyGroup>
12-
<JsonDiffPatchPackageVersion>1.3.0</JsonDiffPatchPackageVersion>
12+
<JsonDiffPatchPackageVersion>1.3.1</JsonDiffPatchPackageVersion>
1313
<Authors>Wei Chen</Authors>
1414
<PackageProjectUrl>https://github.com/weichch/system-text-json-jsondiffpatch</PackageProjectUrl>
1515
<Copyright>Copyright © Wei Chen 2022</Copyright>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.Text.Json.Nodes;
2+
3+
namespace System.Text.Json.JsonDiffPatch.Diffs
4+
{
5+
/// <summary>
6+
/// Represents JSON diff context.
7+
/// </summary>
8+
public class JsonDiffContext
9+
{
10+
private readonly JsonNode _leftNode;
11+
private readonly JsonNode _rightNode;
12+
13+
internal JsonDiffContext(JsonNode left, JsonNode right)
14+
{
15+
_leftNode = left;
16+
_rightNode = right;
17+
}
18+
19+
/// <summary>
20+
/// Gets the left value in comparison.
21+
/// </summary>
22+
/// <typeparam name="T">The type of left value.</typeparam>
23+
public T Left<T>()
24+
{
25+
if (_leftNode is T leftValue)
26+
{
27+
return leftValue;
28+
}
29+
30+
throw new InvalidOperationException($"Type must be '{nameof(JsonNode)}' or derived type.");
31+
}
32+
33+
/// <summary>
34+
/// Gets the right value in comparison.
35+
/// </summary>
36+
/// <typeparam name="T">The type of right value.</typeparam>
37+
public T Right<T>()
38+
{
39+
if (_rightNode is T rightValue)
40+
{
41+
return rightValue;
42+
}
43+
44+
throw new InvalidOperationException($"Type must be '{nameof(JsonNode)}' or derived type.");
45+
}
46+
}
47+
}

src/SystemTextJson.JsonDiffPatch/Diffs/JsonDiffOptions.cs

+6
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ public JsonElementComparison JsonElementComparison
6060
/// </summary>
6161
public IEqualityComparer<JsonValue>? ValueComparer { get; set; }
6262

63+
/// <summary>
64+
/// Gets or sets the filter function to ignore JSON property. To ignore a property,
65+
/// implement this function and return <c>false</c>.
66+
/// </summary>
67+
public Func<string, JsonDiffContext, bool>? PropertyFilter { get; set; }
68+
6369
[MethodImpl(MethodImplOptions.AggressiveInlining)]
6470
internal JsonComparerOptions CreateComparerOptions() => new(JsonElementComparison, ValueComparer);
6571
}

src/SystemTextJson.JsonDiffPatch/Diffs/JsonDiffPatcher.Array.cs

+13-7
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ static void AddDiffResult(
197197
internal static bool MatchArrayItem(ref ArrayItemMatchContext context, JsonDiffOptions? options,
198198
in JsonComparerOptions comparerOptions)
199199
{
200+
// Prefer DeepEquals because LCS is weighted as per deep equality, and deep equality
201+
// usually generates smaller diff
200202
if (context.Left.DeepEquals(context.Right, comparerOptions))
201203
{
202204
context.DeepEqual();
@@ -206,7 +208,7 @@ internal static bool MatchArrayItem(ref ArrayItemMatchContext context, JsonDiffO
206208
if (options is not null && context.Left is JsonObject or JsonArray &&
207209
context.Right is JsonObject or JsonArray)
208210
{
209-
if (FuzzyMatchArrayItem(ref context, options, out var fuzzyResult))
211+
if (FallbackMatchArrayItem(ref context, options, out var fuzzyResult))
210212
{
211213
return fuzzyResult;
212214
}
@@ -220,7 +222,7 @@ internal static bool MatchArrayItem(ref ArrayItemMatchContext context, JsonDiffO
220222
return false;
221223
}
222224

223-
internal static bool MatchArrayItem(
225+
internal static bool MatchArrayValueItem(
224226
ref ArrayItemMatchContext context,
225227
ref JsonValueWrapper wrapperLeft,
226228
ref JsonValueWrapper wrapperRight,
@@ -241,11 +243,12 @@ internal static bool MatchArrayItem(
241243
return false;
242244
}
243245

244-
private static bool FuzzyMatchArrayItem(ref ArrayItemMatchContext context, JsonDiffOptions options,
246+
private static bool FallbackMatchArrayItem(ref ArrayItemMatchContext context, JsonDiffOptions options,
245247
out bool result)
246248
{
247249
result = false;
248250

251+
// Scenario 1: keyed objects or arrays
249252
var keyFinder = options.ArrayObjectItemKeyFinder;
250253
if (keyFinder is not null)
251254
{
@@ -254,25 +257,28 @@ private static bool FuzzyMatchArrayItem(ref ArrayItemMatchContext context, JsonD
254257

255258
if (keyX is null && keyY is null)
256259
{
257-
// Use DeepEquals if both items are not keyed
258260
return false;
259261
}
260262

261263
result = Equals(keyX, keyY);
262264
return true;
263265
}
264266

267+
// Scenario 2: match objects or arrays by position in parent array
265268
if (options.ArrayObjectItemMatchByPosition)
266269
{
267270
if (context.LeftPosition == context.RightPosition)
268271
{
269272
result = true;
270273
return true;
271274
}
275+
}
272276

273-
// We don't return a result for objects at different position
274-
// so that we could still compare them using DeepEquals, or
275-
// return "not equal" if this method is called after.
277+
// Scenario 3: force a later diff operation when property filter is set
278+
if (options.PropertyFilter is not null)
279+
{
280+
result = true;
281+
return true;
276282
}
277283

278284
return false;

src/SystemTextJson.JsonDiffPatch/Diffs/JsonDiffPatcher.Diff.cs

+31-19
Original file line numberDiff line numberDiff line change
@@ -224,37 +224,49 @@ private static void DiffInternal(
224224
{
225225
Debug.Assert(delta.Document is null);
226226

227-
left ??= "";
228-
right ??= "";
227+
// Fast diff scenarios
229228

230-
// Compare two objects
231-
if (left is JsonObject leftObj && right is JsonObject rightObj)
229+
if (ReferenceEquals(left, right))
232230
{
233-
DiffObject(ref delta, leftObj, rightObj, options);
234231
return;
235232
}
236233

237-
// Compare two arrays
238-
if (left is JsonArray leftArr && right is JsonArray rightArr)
234+
if (left is null || right is null)
239235
{
240-
DiffArray(ref delta, leftArr, rightArr, options);
236+
delta.Modified(left, right);
241237
return;
242238
}
243239

244-
// For long texts
245-
// Compare two long texts
246-
if (IsLongText(left, right, options, out var leftText, out var rightText))
240+
// Full diff scenarios
241+
242+
if (left is JsonObject leftObj && right is JsonObject rightObj)
247243
{
248-
Debug.Assert(options is not null);
249-
DiffLongText(ref delta, leftText!, rightText!, options!);
250-
return;
244+
DiffObject(ref delta, leftObj, rightObj, options);
251245
}
252-
253-
// None of the above methods returned a result, fallback to check if both values are deeply equal
254-
// This should also handle DateTime and other CLR types that are strings in JSON
255-
var comparerOptions = options?.CreateComparerOptions() ?? default;
256-
if (!left.DeepEquals(right, comparerOptions))
246+
else if (left is JsonArray leftArr && right is JsonArray rightArr)
247+
{
248+
DiffArray(ref delta, leftArr, rightArr, options);
249+
}
250+
else if (left is JsonValue leftVal && right is JsonValue rightVal)
251+
{
252+
if (IsLongText(leftVal, rightVal, options, out var leftText, out var rightText))
253+
{
254+
Debug.Assert(options is not null);
255+
DiffLongText(ref delta, leftText!, rightText!, options!);
256+
}
257+
else
258+
{
259+
// Check value deep equality as per options, i.e. semantic or raw text equality
260+
var comparerOptions = options?.CreateComparerOptions() ?? default;
261+
if (!leftVal.DeepEquals(rightVal, comparerOptions))
262+
{
263+
delta.Modified(leftVal, rightVal);
264+
}
265+
}
266+
}
267+
else
257268
{
269+
Debug.Assert(left.GetType() != right.GetType(), "Json value type must not equal.");
258270
delta.Modified(left, right);
259271
}
260272
}

src/SystemTextJson.JsonDiffPatch/Diffs/JsonDiffPatcher.Object.cs

+17
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,20 @@ private static void DiffObject(
1717
var leftProperties = (left as IDictionary<string, JsonNode?>).Keys;
1818
var rightProperties = (right as IDictionary<string, JsonNode?>).Keys;
1919

20+
JsonDiffContext? diffContext = null;
21+
var propertyFilter = options?.PropertyFilter;
22+
if (propertyFilter is not null)
23+
{
24+
diffContext = new JsonDiffContext(left, right);
25+
}
26+
2027
foreach (var prop in leftProperties)
2128
{
29+
if (propertyFilter is not null && !propertyFilter(prop, diffContext!))
30+
{
31+
continue;
32+
}
33+
2234
var leftValue = left[prop];
2335
if (!right.TryGetPropertyValue(prop, out var rightValue))
2436
{
@@ -39,6 +51,11 @@ private static void DiffObject(
3951

4052
foreach (var prop in rightProperties)
4153
{
54+
if (propertyFilter is not null && !propertyFilter(prop, diffContext!))
55+
{
56+
continue;
57+
}
58+
4259
var rightValue = right[prop];
4360
if (!left.ContainsKey(prop))
4461
{

src/SystemTextJson.JsonDiffPatch/Diffs/JsonDiffPatcher.Text.cs

+4-9
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ private static void DiffLongText(
4343
}
4444

4545
private static bool IsLongText(
46-
JsonNode? left,
47-
JsonNode? right,
46+
JsonValue left,
47+
JsonValue right,
4848
JsonDiffOptions? options,
4949
out string? leftText,
5050
out string? rightText)
@@ -57,11 +57,6 @@ private static bool IsLongText(
5757
return false;
5858
}
5959

60-
if (left is not JsonValue || right is not JsonValue)
61-
{
62-
return false;
63-
}
64-
6560
while (true)
6661
{
6762
if (options.TextDiffMinLength <= 0)
@@ -70,8 +65,8 @@ private static bool IsLongText(
7065
}
7166

7267
// Perf: This is slower than direct property access
73-
var valueLeft = left.AsValue().GetValue<object>();
74-
var valueRight = right.AsValue().GetValue<object>();
68+
var valueLeft = left.GetValue<object>();
69+
var valueRight = right.GetValue<object>();
7570

7671
if (valueLeft is JsonElement elementLeft && valueRight is JsonElement elementRight)
7772
{

src/SystemTextJson.JsonDiffPatch/Diffs/Lcs.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ public static Lcs Get(Span<JsonNode?> x, Span<JsonNode?> y, JsonDiffOptions? opt
153153

154154
if (x[i - 1] is JsonValue && y[j - 1] is JsonValue)
155155
{
156-
itemMatched = JsonDiffPatcher.MatchArrayItem(ref matchContext,
156+
itemMatched = JsonDiffPatcher.MatchArrayValueItem(ref matchContext,
157157
ref wrapperCacheSpan[i - 1],
158158
ref wrapperCacheSpan[x.Length + j - 1],
159159
options,

0 commit comments

Comments
 (0)