Skip to content

Commit 10912bc

Browse files
authored
Add semantic comparison to DeepEquals and Diff methods (#8)
* Add equality comparer parameter to DeepEquals * Add semantic comparer * Add default diff options * Adding tests for `DeepEquals` * Add docs for release
1 parent 3e9b4fc commit 10912bc

29 files changed

+1793
-193
lines changed

Benchmark.md

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,74 @@
44

55
``` ini
66

7-
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1415 (21H1/May2021Update)
7+
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1586 (21H1/May2021Update)
88
11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores
99
.NET SDK=6.0.200
1010
[Host] : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT
1111
DefaultJob : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT
1212

1313

1414
```
15-
| Method | Mean | Min | Max | P95 | P80 | Allocated |
16-
|-------------------------- |----------:|----------:|----------:|----------:|----------:|----------:|
17-
| Diff_JsonNet | 107.55 μs | 95.29 μs | 122.73 μs | 118.01 μs | 113.43 μs | 132 KB |
18-
| Diff_SystemTextJson | 91.79 μs | 75.05 μs | 126.17 μs | 112.61 μs | 101.81 μs | 70 KB |
19-
| Diff_JsonNet_Rfc | 130.02 μs | 115.41 μs | 156.85 μs | 151.16 μs | 136.42 μs | 150 KB |
20-
| Diff_SystemTextJson_Rfc | 106.12 μs | 95.75 μs | 120.03 μs | 115.57 μs | 110.23 μs | 93 KB |
21-
| Patch_JsonNet | 116.92 μs | 107.06 μs | 137.86 μs | 133.26 μs | 122.34 μs | 162 KB |
22-
| Patch_SystemTextJson | 45.05 μs | 37.98 μs | 56.60 μs | 55.13 μs | 47.42 μs | 37 KB |
23-
| DeepEquals_JsonNet | 69.14 μs | 62.38 μs | 76.96 μs | 74.78 μs | 71.95 μs | 91 KB |
24-
| DeepEquals_SystemTextJson | 53.43 μs | 47.96 μs | 65.03 μs | 60.30 μs | 56.35 μs | 40 KB |
25-
| DeepClone_JsonNet | 51.28 μs | 44.65 μs | 62.34 μs | 58.69 μs | 54.52 μs | 70 KB |
26-
| DeepClone_SystemTextJson | 47.82 μs | 38.78 μs | 59.26 μs | 56.94 μs | 51.11 μs | 45 KB |
15+
| Method | Mean | Min | Max | P95 | P80 | Allocated |
16+
|----------------------------------- |----------:|----------:|----------:|----------:|----------:|----------:|
17+
| Diff_JsonNet | 170.86 μs | 112.81 μs | 250.44 μs | 242.28 μs | 222.06 μs | 132 KB |
18+
| Diff_SystemTextJson | 161.13 μs | 110.60 μs | 272.51 μs | 245.99 μs | 194.82 μs | 70 KB |
19+
| Diff_SystemTextJson_Semantic | 188.61 μs | 121.51 μs | 312.77 μs | 243.53 μs | 223.13 μs | 77 KB |
20+
| Diff_JsonNet_Rfc | 188.54 μs | 143.31 μs | 305.56 μs | 276.16 μs | 231.28 μs | 150 KB |
21+
| Diff_SystemTextJson_Rfc | 181.22 μs | 107.66 μs | 287.07 μs | 248.40 μs | 223.91 μs | 92 KB |
22+
| Patch_JsonNet | 205.86 μs | 123.47 μs | 319.94 μs | 277.28 μs | 242.98 μs | 162 KB |
23+
| Patch_SystemTextJson | 78.75 μs | 54.53 μs | 113.21 μs | 98.50 μs | 88.62 μs | 37 KB |
24+
| DeepEquals_JsonNet | 122.18 μs | 82.64 μs | 196.77 μs | 169.75 μs | 148.24 μs | 91 KB |
25+
| DeepEquals_SystemTextJson | 89.19 μs | 55.40 μs | 151.38 μs | 123.07 μs | 116.02 μs | 39 KB |
26+
| DeepEquals_SystemTextJson_Semantic | 119.93 μs | 79.38 μs | 189.04 μs | 173.98 μs | 150.50 μs | 46 KB |
27+
| DeepClone_JsonNet | 89.48 μs | 51.36 μs | 136.46 μs | 121.85 μs | 104.55 μs | 70 KB |
28+
| DeepClone_SystemTextJson | 64.91 μs | 38.96 μs | 97.91 μs | 87.33 μs | 77.57 μs | 45 KB |
2729

2830

2931
## Large JSON object
3032

3133
``` ini
3234

33-
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1415 (21H1/May2021Update)
35+
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1586 (21H1/May2021Update)
3436
11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores
3537
.NET SDK=6.0.200
3638
[Host] : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT
3739
DefaultJob : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT
3840

3941

4042
```
41-
| Method | Mean | Min | Max | P95 | P80 | Allocated |
42-
|-------------------------- |----------:|----------:|----------:|----------:|----------:|----------:|
43-
| Diff_JsonNet | 11.155 ms | 9.132 ms | 13.732 ms | 12.802 ms | 11.896 ms | 4 MB |
44-
| Diff_SystemTextJson | 8.375 ms | 7.334 ms | 9.132 ms | 8.978 ms | 8.690 ms | 3 MB |
45-
| Diff_JsonNet_Rfc | 12.774 ms | 9.807 ms | 17.990 ms | 17.479 ms | 13.374 ms | 6 MB |
46-
| Diff_SystemTextJson_Rfc | 11.664 ms | 10.341 ms | 12.918 ms | 12.668 ms | 12.142 ms | 5 MB |
47-
| Patch_JsonNet | 12.146 ms | 10.344 ms | 13.576 ms | 13.302 ms | 12.647 ms | 5 MB |
48-
| Patch_SystemTextJson | 4.693 ms | 2.422 ms | 5.685 ms | 5.412 ms | 5.061 ms | 2 MB |
49-
| DeepEquals_JsonNet | 4.164 ms | 2.584 ms | 5.424 ms | 4.917 ms | 4.652 ms | 2 MB |
50-
| DeepEquals_SystemTextJson | 3.254 ms | 2.071 ms | 3.837 ms | 3.709 ms | 3.499 ms | 2 MB |
51-
| DeepClone_JsonNet | 3.997 ms | 2.529 ms | 5.165 ms | 4.772 ms | 4.531 ms | 2 MB |
52-
| DeepClone_SystemTextJson | 3.396 ms | 2.085 ms | 3.981 ms | 3.811 ms | 3.609 ms | 2 MB |
43+
| Method | Mean | Min | Max | P95 | P80 | Allocated |
44+
|----------------------------------- |----------:|---------:|----------:|----------:|----------:|----------:|
45+
| Diff_JsonNet | 6.825 ms | 4.746 ms | 13.074 ms | 11.310 ms | 9.284 ms | 4 MB |
46+
| Diff_SystemTextJson | 5.898 ms | 4.996 ms | 7.424 ms | 6.910 ms | 6.329 ms | 3 MB |
47+
| Diff_SystemTextJson_Semantic | 6.466 ms | 4.683 ms | 10.261 ms | 8.130 ms | 7.464 ms | 4 MB |
48+
| Diff_JsonNet_Rfc | 12.654 ms | 8.208 ms | 17.506 ms | 15.528 ms | 14.234 ms | 6 MB |
49+
| Diff_SystemTextJson_Rfc | 10.018 ms | 6.678 ms | 16.484 ms | 13.998 ms | 11.816 ms | 5 MB |
50+
| Patch_JsonNet | 9.128 ms | 5.115 ms | 14.050 ms | 11.422 ms | 10.736 ms | 5 MB |
51+
| Patch_SystemTextJson | 3.772 ms | 2.576 ms | 5.572 ms | 4.933 ms | 4.421 ms | 2 MB |
52+
| DeepEquals_JsonNet | 4.549 ms | 3.280 ms | 6.667 ms | 6.059 ms | 5.529 ms | 2 MB |
53+
| DeepEquals_SystemTextJson | 3.673 ms | 2.321 ms | 6.153 ms | 5.028 ms | 4.303 ms | 2 MB |
54+
| DeepEquals_SystemTextJson_Semantic | 4.630 ms | 2.824 ms | 7.049 ms | 6.303 ms | 5.708 ms | 2 MB |
55+
| DeepClone_JsonNet | 3.294 ms | 2.014 ms | 5.439 ms | 4.518 ms | 4.190 ms | 2 MB |
56+
| DeepClone_SystemTextJson | 1.761 ms | 1.475 ms | 2.107 ms | 2.000 ms | 1.927 ms | 2 MB |
57+
58+
59+
## `DeepEquals` benchmarks
60+
61+
``` ini
62+
63+
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1586 (21H1/May2021Update)
64+
11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores
65+
.NET SDK=6.0.200
66+
[Host] : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT
67+
DefaultJob : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT
68+
69+
70+
```
71+
| Method | Mean | Min | Max | P95 | P80 | Allocated |
72+
|-------------------------- |-----------:|-----------:|-----------:|-----------:|-----------:|----------:|
73+
| JsonNet_Array | 1,198.9 ns | 907.5 ns | 1,704.5 ns | 1,557.5 ns | 1,349.9 ns | 1,248 B |
74+
| JsonNet_ParseArray | 2,879.1 ns | 2,216.4 ns | 3,697.3 ns | 3,361.4 ns | 3,124.2 ns | 5,920 B |
75+
| SystemTextJson_Array | 857.1 ns | 643.5 ns | 1,312.3 ns | 1,183.3 ns | 1,033.4 ns | 864 B |
76+
| SystemTextJson_ParseArray | 2,141.7 ns | 2,027.8 ns | 2,188.0 ns | 2,184.4 ns | 2,159.7 ns | 1,264 B |
5377

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# system-text-json-jsondiffpatch
1+
# SystemTextJson.JsonDiffPatch
22

33
High-performance, low-allocating JSON objects diff and patch extension for System.Text.Json.
44

@@ -12,6 +12,7 @@ High-performance, low-allocating JSON objects diff and patch extension for Syste
1212
- Support smart array diffing (e.g. move detect) using LCS and custom array item matcher
1313
- _(Only when not using RFC 6902 format)_ Support diffing long text using [google-diff-match-patch](http://code.google.com/p/google-diff-match-patch/), or write your own diff algorithm
1414
- Bonus `JsonNode.DeepClone` and `JsonNode.DeepEquals` methods
15+
- Bouns `JsonValueComparer` that implements semantic comparison of two `JsonValue` objects (including `JsonValue` backed by `JsonElement`)
1516
- JSON assert for xUnit, MSTest v2 and NUnit with customizable delta output
1617

1718
## Install
@@ -57,6 +58,22 @@ JsonNode? cloned = node.DeepClone();
5758
var node1 = JsonNode.Parse(...);
5859
var node2 = JsonNode.Parse(...);
5960
bool equal = node1.DeepEquals(node2);
61+
bool semanticEqual = node1.DeepEquals(node2, JsonElementComparison.Semantic);
62+
```
63+
64+
### Semantic Value Comparison
65+
```csharp
66+
var node1 = JsonNode.Parse("[\"2019-11-27\"]").First().AsValue();
67+
var node2 = JsonNode.Parse("[\"2019-11-27T00:00:00.000\"]").First().AsValue();
68+
69+
// dateCompare is 0
70+
var dateCompare = JsonValueComparer.Compare(node1, node2);
71+
72+
var node3 = JsonNode.Parse("[1]").First().AsValue();
73+
var node4 = JsonNode.Parse("[1.00]").First().AsValue();
74+
75+
// numCompare is 0
76+
var numCompare = JsonValueComparer.Compare(node3, node4);
6077
```
6178

6279
### Patch & Unpatch

ReleaseNotes.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Release Notes
2+
3+
## 1.1.0
4+
- Added `JsonValueComparer` that implements semantic comparison of two `JsonValue` objects (including the ones backed by `JsonElement`)
5+
- **[BREAKING CHANGE]** `Diff` method no longer uses `object.Equals` to compare values encapsulated in `JsonValue<T>`. `JsonValueComparer` is used instead
6+
- Added semantic equality to `DeepEquals` method
7+
- Added options to `JsonDiffOptions` to enable semantic diff
8+
- Added `JsonDiffPatcher.DefaultOptions` for customizing default diff options
9+
10+
## 1.0.0
11+
- Initial release

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
</PropertyGroup>
1818

1919
<PropertyGroup>
20-
<Version>1.0.0</Version>
20+
<Version>1.1.0</Version>
2121
<AssemblyVersion>1.0.0.0</AssemblyVersion>
2222
<FileVersion>1.0.0.0</FileVersion>
2323
</PropertyGroup>

src/SystemTextJson.JsonDiffPatch/Diffs/DefaultArrayItemComparer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public bool MatchArrayItem(ref ArrayItemMatchContext context)
2929
}
3030
}
3131

32-
if (context.Left.DeepEquals(context.Right))
32+
if (context.Left.DeepEquals(context.Right, _options.CreateComparerOptions()))
3333
{
3434
context.DeepEqual();
3535
return true;

src/SystemTextJson.JsonDiffPatch/Diffs/JsonDiffOptions.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Text.Json.JsonDiffPatch.Diffs;
1+
using System.Collections.Generic;
2+
using System.Text.Json.JsonDiffPatch.Diffs;
23
using System.Text.Json.Nodes;
34

45
namespace System.Text.Json.JsonDiffPatch
@@ -9,6 +10,7 @@ namespace System.Text.Json.JsonDiffPatch
910
public class JsonDiffOptions
1011
{
1112
internal static readonly JsonDiffOptions Default = new();
13+
private JsonComparerOptions _comparerOptions;
1214

1315
/// <summary>
1416
/// Specifies whether to suppress detect array move. Default value is <c>false</c>.
@@ -60,5 +62,26 @@ public class JsonDiffOptions
6062
/// Gets or sets the function to diff long texts.
6163
/// </summary>
6264
public Func<string, string, string?>? TextDiffProvider { get; set; }
65+
66+
/// <summary>
67+
/// Gets or sets the mode to compare two <see cref="JsonElement"/> instances.
68+
/// </summary>
69+
public JsonElementComparison JsonElementComparison { get; set; }
70+
71+
/// <summary>
72+
/// Gets or sets the <see cref="JsonValue"/> comparer.
73+
/// </summary>
74+
public IEqualityComparer<JsonValue>? ValueComparer { get; set; }
75+
76+
internal ref JsonComparerOptions CreateComparerOptions()
77+
{
78+
if (JsonElementComparison != _comparerOptions.JsonElementComparison
79+
|| !Equals(ValueComparer, _comparerOptions.ValueComparer))
80+
{
81+
_comparerOptions = new JsonComparerOptions(JsonElementComparison, ValueComparer);
82+
}
83+
84+
return ref _comparerOptions;
85+
}
6386
}
6487
}

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,8 @@ private static void DiffInternal(
222222
JsonDiffOptions? options)
223223
{
224224
Debug.Assert(delta.Document is null);
225-
226-
options ??= JsonDiffOptions.Default;
225+
226+
options ??= DefaultOptions?.Invoke() ?? JsonDiffOptions.Default;
227227

228228
left ??= "";
229229
right ??= "";
@@ -252,10 +252,9 @@ private static void DiffInternal(
252252

253253
// None of the above methods returned a result, fallback to check if both values are deeply equal
254254
// This should also handle DateTime and other CLR types that are strings in JSON
255-
if (!left.DeepEquals(right))
255+
if (!left.DeepEquals(right, options.CreateComparerOptions()))
256256
{
257257
delta.Modified(left, right);
258-
return;
259258
}
260259
}
261260
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System.Collections.Generic;
2+
using System.Text.Json.Nodes;
3+
4+
namespace System.Text.Json.JsonDiffPatch
5+
{
6+
/// <summary>
7+
/// Represents the options for internal JSON comparer.
8+
/// </summary>
9+
public readonly struct JsonComparerOptions
10+
{
11+
/// <summary>
12+
/// Creates an instance of the options.
13+
/// </summary>
14+
public JsonComparerOptions(JsonElementComparison jsonElementComparison,
15+
IEqualityComparer<JsonValue>? valueComparer)
16+
{
17+
JsonElementComparison = jsonElementComparison;
18+
ValueComparer = valueComparer;
19+
}
20+
21+
/// <summary>
22+
/// Gets the mode to compare two <see cref="JsonElement"/> instances.
23+
/// </summary>
24+
public JsonElementComparison JsonElementComparison { get; }
25+
26+
/// <summary>
27+
/// Gets the value comparer.
28+
/// </summary>
29+
public IEqualityComparer<JsonValue>? ValueComparer { get; }
30+
}
31+
}

0 commit comments

Comments
 (0)