Skip to content

Commit 7278d2a

Browse files
authored
New DeepEquals for JsonDocument and JsonElement (#25)
* Add default DeepEquals comparison * Add DeepEquals to JsonElement and JsonDocument * Refactor deep equals * Move node tests to a new directory * Add tests for JsonElement * Add tests for JsonDocument * Paper work for 1.3.0
1 parent f73d599 commit 7278d2a

38 files changed

+2046
-1240
lines changed

Benchmark.md

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1645 (21H1/May2021Update)
1515

1616
## Comparison Modes
1717

18-
| Method | FileSize | Mean | Median | Min | Max | P80 | P95 | Allocated |
19-
|--------- |--------- |---------:|---------:|---------:|---------:|---------:|---------:|----------:|
20-
| RawText | Small | 100.9 μs | 100.3 μs | 98.36 μs | 105.7 μs | 102.0 μs | 104.9 μs | 77 KB |
21-
| Semantic | Small | 102.2 μs | 101.9 μs | 99.37 μs | 107.0 μs | 103.1 μs | 105.6 μs | 76 KB |
18+
| Method | FileSize | Mean | Median | Min | Max | P80 | P95 | Allocated |
19+
|--------- |--------- |----------:|----------:|----------:|----------:|----------:|----------:|----------:|
20+
| RawText | Small | 94.05 μs | 94.09 μs | 93.13 μs | 95.36 μs | 94.35 μs | 94.80 μs | 75 KB |
21+
| Semantic | Small | 104.65 μs | 104.27 μs | 102.67 μs | 107.86 μs | 105.55 μs | 107.76 μs | 75 KB |
2222

2323
\* _All benchmarks are generated using the same small JSON object used in the **System.Text.Json vs Newtonsoft Json** section below, with array move detection enabled (default)._
2424

@@ -28,23 +28,25 @@ BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1645 (21H1/May2021Update)
2828

2929
| Method | FileSize | Mean | Median | Min | Max | P80 | P95 | Allocated |
3030
|------------------- |--------- |------------:|------------:|------------:|------------:|------------:|------------:|----------:|
31-
| **SystemTextJson** | **Small** | **79.09 μs** | **78.25 μs** | **75.72 μs** | **84.89 μs** | **81.91 μs** | **84.16 μs** | **66 KB** |
32-
| JsonNet | Small | 91.98 μs | 92.15 μs | 90.06 μs | 94.50 μs | 92.77 μs | 93.78 μs | 132 KB |
33-
| SystemTextJson_Rfc | Small | 94.88 μs | 94.74 μs | 93.34 μs | 97.80 μs | 95.69 μs | 96.86 μs | 87 KB |
34-
| JsonNet_Rfc | Small | 106.41 μs | 106.01 μs | 103.58 μs | 110.69 μs | 107.38 μs | 109.75 μs | 150 KB |
35-
| **SystemTextJson** | **Large** | **3,717.44 μs** | **3,700.53 μs** | **3,577.00 μs** | **3,913.15 μs** | **3,766.22 μs** | **3,901.23 μs** | **3,258 KB** |
36-
| JsonNet | Large | 4,104.18 μs | 4,085.60 μs | 3,922.80 μs | 4,343.10 μs | 4,199.56 μs | 4,273.98 μs | 4,386 KB |
37-
| SystemTextJson_Rfc | Large | 4,900.93 μs | 4,890.86 μs | 4,772.28 μs | 5,128.16 μs | 4,958.30 μs | 5,021.94 μs | 4,561 KB |
38-
| JsonNet_Rfc | Large | 5,569.83 μs | 5,535.12 μs | 5,354.93 μs | 5,976.46 μs | 5,682.46 μs | 5,822.63 μs | 6,147 KB |
31+
| **SystemTextJson** | **Small** | **76.93 μs** | **76.88 μs** | **75.62 μs** | **79.28 μs** | **77.43 μs** | **78.11 μs** | **67 KB** |
32+
| JsonNet | Small | 84.97 μs | 84.75 μs | 83.72 μs | 87.68 μs | 85.64 μs | 86.38 μs | 132 KB |
33+
| SystemTextJson_Rfc | Small | 91.88 μs | 91.71 μs | 90.70 μs | 95.01 μs | 92.37 μs | 94.37 μs | 89 KB |
34+
| JsonNet_Rfc | Small | 102.15 μs | 102.10 μs | 100.49 μs | 104.37 μs | 102.58 μs | 103.29 μs | 150 KB |
35+
| **SystemTextJson** | **Large** | **3,739.64 μs** | **3,734.25 μs** | **3,626.78 μs** | **3,902.76 μs** | **3,781.22 μs** | **3,844.92 μs** | **3,365 KB** |
36+
| JsonNet | Large | 3,846.70 μs | 3,850.62 μs | 3,760.20 μs | 3,917.07 μs | 3,887.43 μs | 3,896.80 μs | 4,386 KB |
37+
| SystemTextJson_Rfc | Large | 4,897.11 μs | 4,868.30 μs | 4,722.99 μs | 5,196.12 μs | 4,930.06 μs | 5,159.49 μs | 4,667 KB |
38+
| JsonNet_Rfc | Large | 5,260.99 μs | 5,249.26 μs | 5,121.82 μs | 5,487.74 μs | 5,322.84 μs | 5,460.47 μs | 6,147 KB |
3939

4040
### DeepEquals
4141

42-
| Method | FileSize | Mean | Median | Min | Max | P80 | P95 | Allocated |
43-
|--------------- |--------- |------------:|------------:|------------:|------------:|------------:|------------:|----------:|
44-
| **SystemTextJson** | **Small** | **52.92 μs** | **52.89 μs** | **52.00 μs** | **54.46 μs** | **53.31 μs** | **53.90 μs** | **39 KB** |
45-
| JsonNet | Small | 58.82 μs | 58.77 μs | 57.78 μs | 60.41 μs | 59.16 μs | 59.74 μs | 91 KB |
46-
| **SystemTextJson** | **Large** | **2,099.55 μs** | **2,090.78 μs** | **1,963.92 μs** | **2,302.56 μs** | **2,161.80 μs** | **2,223.10 μs** | **1,631 KB** |
47-
| JsonNet | Large | 2,296.54 μs | 2,293.76 μs | 2,239.68 μs | 2,393.52 μs | 2,323.09 μs | 2,378.88 μs | 2,426 KB |
42+
| Method | FileSize | Mean | Median | Min | Max | P80 | P95 | Allocated |
43+
|------------------------ |--------- |------------:|------------:|------------:|------------:|------------:|------------:|----------:|
44+
| **SystemTextJson_Node** | **Small** | **55.10 μs** | **54.96 μs** | **54.14 μs** | **56.93 μs** | **55.49 μs** | **56.57 μs** | **38 KB** |
45+
| SystemTextJson_Document | Small | 40.63 μs | 40.58 μs | 40.08 μs | 41.27 μs | 40.80 μs | 41.12 μs | 26 KB |
46+
| JsonNet | Small | 57.84 μs | 57.62 μs | 57.17 μs | 59.40 μs | 58.07 μs | 58.98 μs | 91 KB |
47+
| **SystemTextJson_Node** | **Large** | **2,143.34 μs** | **2,125.71 μs** | **2,048.46 μs** | **2,328.43 μs** | **2,194.35 μs** | **2,266.60 μs** | **1,571 KB** |
48+
| SystemTextJson_Document | Large | 1,372.31 μs | 1,371.00 μs | 1,352.61 μs | 1,391.00 μs | 1,379.30 μs | 1,388.30 μs | 920 KB |
49+
| JsonNet | Large | 2,208.71 μs | 2,209.77 μs | 2,182.51 μs | 2,246.30 μs | 2,223.80 μs | 2,235.96 μs | 2,426 KB |
4850

4951
### Patch
5052

README.md

Lines changed: 61 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,40 @@ High-performance, low-allocating JSON object diff and patch extension for System
88

99
- Compatible with [jsondiffpatch delta format](https://github.com/benjamine/jsondiffpatch/blob/master/docs/deltas.md)
1010
- Support generating patch document in RFC 6902 JSON Patch format
11-
- Target latest .NET Standard and .NET Framework 4.6.1 (for legacy apps) and leverage latest .NET features
11+
- Target latest **.NET Standard** and **.NET Framework 4.6.1** (for legacy apps) and leverage latest .NET features
1212
- Alternative to [jsondiffpatch.net](https://github.com/wbish/jsondiffpatch.net) which is based on Newtonsoft.Json
1313
- Fast large JSON document diffing with less memory consumption (see [benchmark](https://github.com/weichch/system-text-json-jsondiffpatch/blob/main/Benchmark.md))
1414
- Support smart array diffing (e.g. move detect) using LCS (Longest Common Subsequence) and custom array item matcher
1515
- _(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
16-
- Bonus `JsonNode.DeepClone` and `JsonNode.DeepEquals` methods
17-
- Bouns [`JsonValueComparer`](https://github.com/weichch/system-text-json-jsondiffpatch/blob/main/src/SystemTextJson.JsonDiffPatch/JsonValueComparer.cs) that implements semantic comparison of two `JsonValue` objects (including `JsonValue` backed by `JsonElement`)
16+
- Bonus `DeepEquals` method for comparing `JsonDocument`, `JsonElement` and `JsonNode`
17+
- Bonus `DeepClone` method
18+
- Bonus [`JsonValueComparer`](https://github.com/weichch/system-text-json-jsondiffpatch/blob/main/src/SystemTextJson.JsonDiffPatch/JsonValueComparer.cs) that implements semantic comparison of two `JsonValue` objects
1819
- JSON assert for xUnit, MSTest v2 and NUnit with customizable delta output
1920

2021
## Install
2122

2223
#### JsonDiffPatch
2324

2425
```
25-
Install-Package SystemTextJson.JsonDiffPatch
26+
PM> Install-Package SystemTextJson.JsonDiffPatch
2627
```
2728

2829
#### xUnit Assert
2930

3031
```
31-
Install-Package SystemTextJson.JsonDiffPatch.Xunit
32+
PM> Install-Package SystemTextJson.JsonDiffPatch.Xunit
3233
```
3334

3435
#### MSTest v2 Assert
3536

3637
```
37-
Install-Package SystemTextJson.JsonDiffPatch.MSTest
38+
PM> Install-Package SystemTextJson.JsonDiffPatch.MSTest
3839
```
3940

4041
#### NUnit Assert
4142

4243
```
43-
Install-Package SystemTextJson.JsonDiffPatch.NUnit
44+
PM> Install-Package SystemTextJson.JsonDiffPatch.NUnit
4445
```
4546

4647
## Usage
@@ -51,80 +52,91 @@ Install-Package SystemTextJson.JsonDiffPatch.NUnit
5152
// Diff JsonNode
5253
var node1 = JsonNode.Parse("{\"foo\":\"bar\"}");
5354
var node2 = JsonNode.Parse("{\"baz\":\"qux\", \"foo\":\"bar\"}");
54-
JsonNode? diff = node1.Diff(node2);
55+
var diff = node1.Diff(node2);
5556
// Diff with options
56-
JsonNode? diff = node1.Diff(node2, new JsonDiffOptions
57+
var diff = node1.Diff(node2, new JsonDiffOptions
5758
{
5859
JsonElementComparison = JsonElementComparison.Semantic
5960
});
6061
// Diff and convert delta into RFC 6902 JSON Patch format
61-
JsonNode? diff = node1.Diff(node2, new JsonPatchDeltaFormatter());
62+
var diff = node1.Diff(node2, new JsonPatchDeltaFormatter());
6263
// Diff JSON files
63-
JsonNode? diff = JsonDiffPatcher.DiffFile(file1, file2);
64+
var diff = JsonDiffPatcher.DiffFile(file1, file2);
6465
// Diff Span<byte>
65-
JsonNode? diff = JsonDiffPatcher.Diff(span1, span2);
66+
var diff = JsonDiffPatcher.Diff(span1, span2);
6667
// Diff streams
67-
JsonNode? diff = JsonDiffPatcher.Diff(stream1, stream2);
68+
var diff = JsonDiffPatcher.Diff(stream1, stream2);
6869
// Diff JSON strings
69-
JsonNode? diff = JsonDiffPatcher.Diff(json1, json2);
70+
var diff = JsonDiffPatcher.Diff(json1, json2);
7071
// Diff JSON readers
71-
JsonNode? diff = JsonDiffPatcher.Diff(ref reader1, ref reader2);
72+
var diff = JsonDiffPatcher.Diff(ref reader1, ref reader2);
7273
```
7374

74-
### DeepClone
75+
### Patch & Unpatch
7576

7677
```csharp
77-
var node = JsonNode.Parse("{\"foo\":\"bar\"}");
78-
JsonNode? cloned = node.DeepClone();
78+
var node1 = JsonNode.Parse("{\"foo\":\"bar\"}");
79+
var node2 = JsonNode.Parse("{\"baz\":\"qux\", \"foo\":\"bar\"}");
80+
var diff = node1.Diff(node2);
81+
// In-place patch
82+
JsonDiffPatcher.Patch(ref node1, diff);
83+
// Clone & patch
84+
var patched = node1.PatchNew(diff);
85+
// In-place unpatch
86+
JsonDiffPatcher.ReversePatch(ref node1, diff);
87+
// Clone & unpatch
88+
var patched = node1.ReversePatchNew(diff);
7989
```
8090

8191
### DeepEquals
8292

8393
```csharp
84-
var node1 = JsonNode.Parse("{\"foo\":1.0}");
85-
var node2 = JsonNode.Parse("{\"foo\":1}");
86-
// equal is false
87-
bool equal = node1.DeepEquals(node2);
88-
// semanticEqual is true
89-
bool semanticEqual = node1.DeepEquals(node2, JsonElementComparison.Semantic);
90-
```
91-
92-
### Semantic Value Comparison
93-
```csharp
94-
var node1 = JsonNode.Parse("\"2019-11-27\"");
95-
var node2 = JsonNode.Parse("\"2019-11-27T00:00:00.000\"");
96-
// dateCompare is 0
97-
var dateCompare = JsonValueComparer.Compare(node1, node2);
94+
// JsonDocument
95+
var doc1 = JsonDocument.Parse("{\"foo\":1}");
96+
var doc2 = JsonDocument.Parse("{\"foo\":1.0}");
97+
var equal = doc1.DeepEquals(doc2);
98+
var textEqual = doc1.DeepEquals(doc2, JsonElementComparison.RawText);
99+
var semanticEqual = doc1.DeepEquals(doc2, JsonElementComparison.Semantic);
98100

99-
var node3 = JsonNode.Parse("1");
100-
var node4 = JsonNode.Parse("1.00");
101-
// numCompare is 0
102-
var numCompare = JsonValueComparer.Compare(node3, node4);
101+
// JsonNode
102+
var node1 = JsonNode.Parse("{\"foo\":1}");
103+
var node2 = JsonNode.Parse("{\"foo\":1.0}");
104+
var equal = node1.DeepEquals(node2);
105+
var textEqual = node1.DeepEquals(node2, JsonElementComparison.RawText);
106+
var semanticEqual = node1.DeepEquals(node2, JsonElementComparison.Semantic);
103107
```
104108

105-
### Patch & Unpatch
109+
### DeepClone
106110

107111
```csharp
108-
var node1 = JsonNode.Parse("{\"foo\":\"bar\"}");
109-
var node2 = JsonNode.Parse("{\"baz\":\"qux\", \"foo\":\"bar\"}");
110-
JsonNode? diff = node1.Diff(node2);
111-
// In-place patch
112-
JsonDiffPatcher.Patch(ref node1, diff);
113-
// Clone & patch
114-
node1.PatchNew(diff);
115-
// In-place unpatch
116-
JsonDiffPatcher.ReversePatch(ref node1, diff);
117-
// Clone & unpatch
118-
node1.ReversePatchNew(diff);
112+
var node = JsonNode.Parse("{\"foo\":\"bar\"}");
113+
var cloned = node.DeepClone();
119114
```
120115

121116
### Default Options
122117

123118
```csharp
119+
// Default diff options
124120
JsonDiffPatcher.DefaultOptions = () => new JsonDiffOptions
125121
{
126122
JsonElementComparison = JsonElementComparison.Semantic
127123
};
124+
125+
// Default comparison mode for DeepEquals
126+
JsonDiffPatcher.DefaultComparison = JsonElementComparison.Semantic;
127+
```
128+
129+
### Semantic Value Comparison
130+
```csharp
131+
var node1 = JsonNode.Parse("\"2019-11-27\"");
132+
var node2 = JsonNode.Parse("\"2019-11-27T00:00:00.000\"");
133+
// dateCompare is 0
134+
var dateCompare = JsonValueComparer.Compare(node1, node2);
135+
136+
var node3 = JsonNode.Parse("1");
137+
var node4 = JsonNode.Parse("1.00");
138+
// numCompare is 0
139+
var numCompare = JsonValueComparer.Compare(node3, node4);
128140
```
129141

130142
### Assert (Unit Testing)
@@ -178,4 +190,4 @@ Delta:
178190

179191
## Benchmark
180192

181-
[Benchmark results](https://github.com/weichch/system-text-json-jsondiffpatch/blob/main/Benchmark.md) were generated using example objects [here](https://github.com/weichch/system-text-json-jsondiffpatch/tree/main/test/Examples) and benchmark tests [here](https://github.com/weichch/system-text-json-jsondiffpatch/tree/main/test/SystemTextJson.JsonDiffPatch.Benchmark/).
193+
See detailed [benchmark results](https://github.com/weichch/system-text-json-jsondiffpatch/blob/main/Benchmark.md).

ReleaseNotes.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Release Notes
22

3+
## 1.3.0
4+
5+
- **Added `DeepEquals` implementation for `JsonDocument` and `JsonElement`**
6+
- Performance improvements in raw text comparison mode
7+
- Removed unnecessary allocation when default diff option is used
8+
- Removed one `DeepEquals` overload that was accidentally exposed as a public method
9+
310
## 1.2.0
411

512
- Major performance improvement in array comparison

src/SystemTextJson.JsonDiffPatch/Diffs/JsonDiffOptions.cs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Generic;
2+
using System.Runtime.CompilerServices;
23
using System.Text.Json.JsonDiffPatch.Diffs;
34
using System.Text.Json.Nodes;
45

@@ -9,8 +10,7 @@ namespace System.Text.Json.JsonDiffPatch
910
/// </summary>
1011
public class JsonDiffOptions
1112
{
12-
internal static readonly JsonDiffOptions Default = new();
13-
private JsonComparerOptions _comparerOptions;
13+
private JsonElementComparison? _jsonElementComparison;
1414

1515
/// <summary>
1616
/// Specifies whether to suppress detect array move. Default value is <c>false</c>.
@@ -49,22 +49,18 @@ public class JsonDiffOptions
4949
/// <summary>
5050
/// Gets or sets the mode to compare two <see cref="JsonElement"/> instances.
5151
/// </summary>
52-
public JsonElementComparison JsonElementComparison { get; set; }
52+
public JsonElementComparison JsonElementComparison
53+
{
54+
get => _jsonElementComparison ?? JsonDiffPatcher.DefaultComparison;
55+
set => _jsonElementComparison = value;
56+
}
5357

5458
/// <summary>
5559
/// Gets or sets the <see cref="JsonValue"/> comparer.
5660
/// </summary>
5761
public IEqualityComparer<JsonValue>? ValueComparer { get; set; }
5862

59-
internal ref JsonComparerOptions CreateComparerOptions()
60-
{
61-
if (JsonElementComparison != _comparerOptions.JsonElementComparison
62-
|| !ReferenceEquals(ValueComparer, _comparerOptions.ValueComparer))
63-
{
64-
_comparerOptions = new JsonComparerOptions(JsonElementComparison, ValueComparer);
65-
}
66-
67-
return ref _comparerOptions;
68-
}
63+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
64+
internal JsonComparerOptions CreateComparerOptions() => new(JsonElementComparison, ValueComparer);
6965
}
7066
}

0 commit comments

Comments
 (0)