Skip to content

Commit 7abec50

Browse files
authored
Add jsonpatch formatter (#3)
* Add formatter interface * Refactor formatter * Add JsonPatch formatter * Complete JsonPatchDeltaFormatter * Fix JsonPatch ordering * Add more benchmarks and more target frameworks
1 parent 4bf5093 commit 7abec50

35 files changed

+18714
-884
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,3 +348,6 @@ MigrationBackup/
348348

349349
# Ionide (cross platform F# VS Code tools) working folder
350350
.ionide/
351+
352+
# Rider
353+
.idea/

README.md

Lines changed: 56 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,14 @@ High-performance, low-allocating JSON objects diff and patch extension for `Syst
55
## Features
66

77
- Use [jsondiffpatch](https://github.com/benjamine/jsondiffpatch) delta format described [here](https://github.com/benjamine/jsondiffpatch/blob/master/docs/deltas.md)
8-
- Target `.NET Standard 2.0` and leverage latest .NET features
9-
- Similar diff experience as [jsondiffpatch.net](https://github.com/wbish/jsondiffpatch.net) (based on Newtonsoft.Json)
8+
- Target latest `.NET Standard` and `.NET Framework 4.6.1` (for legacy apps) and leverage latest .NET features
9+
- Alternative to [jsondiffpatch.net](https://github.com/wbish/jsondiffpatch.net) which is based on `Newtonsoft.Json`
10+
- Support generating patch document in RFC 6902 JSON Patch format
1011
- Fast large JSON document diffing with less memory consumption
1112
- Support smart array diffing (e.g. move detect) using LCS and custom array item matcher
12-
- Support diffing long text using [google-diff-match-patch](http://code.google.com/p/google-diff-match-patch/), or write your own diff algorithm
13+
- _(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
1314
- `JsonNode.DeepClone` and `JsonNode.DeepEquals` methods
1415

15-
- (_Under development_) formatters etc
16-
1716
# Install
1817

1918
Install from [NuGet.org](https://www.nuget.org/packages/SystemTextJson.JsonDiffPatch/):
@@ -36,11 +35,14 @@ JsonNode? diff = JsonDiffPatcher.Diff(stream1, stream2);
3635
JsonNode? diff = JsonDiffPatcher.Diff(json1, json2);
3736
// Diff JSON readers
3837
JsonNode? diff = JsonDiffPatcher.Diff(ref reader1, ref reader2);
39-
40-
// Diff mutable JsonNode objects
38+
// Diff JsonNode objects
4139
var node1 = JsonNode.Parse(...);
4240
var node2 = JsonNode.Parse(...);
4341
JsonNode? diff = node1.Diff(node2);
42+
// Diff with options
43+
JsonNode? diff = node1.Diff(node2, new JsonDiffOptions { ... });
44+
// Diff and convert delta into RFC 6902 JSON Patch format
45+
JsonNode? diff = node1.Diff(node2, new JsonPatchDeltaFormatter());
4446
```
4547

4648
### DeepClone
@@ -70,86 +72,58 @@ JsonDiffPatcher.Patch(ref node1, diff);
7072
JsonDiffPatcher.ReversePatch(ref node1, diff);
7173
```
7274

73-
### Options
75+
## Benchmark
7476

75-
```csharp
76-
public struct JsonDiffOptions
77-
{
78-
/// <summary>
79-
/// Specifies whether to suppress detect array move. Default value is <c>false</c>.
80-
/// </summary>
81-
public bool SuppressDetectArrayMove { get; set; }
82-
83-
/// <summary>
84-
/// Gets or sets the function to match array items.
85-
/// </summary>
86-
public ArrayItemMatch? ArrayItemMatcher { get; set; }
87-
88-
/// <summary>
89-
/// Gets or sets the function to find key of a <see cref="JsonObject"/>
90-
/// or <see cref="JsonArray"/>. This is used when matching array items by
91-
/// their keys. If this function returns <c>null</c>, the items being
92-
/// compared are treated as "not keyed". When comparing two "not keyed"
93-
/// objects, their contents are compared. This function is only used when
94-
/// <see cref="ArrayItemMatcher"/> is set to <c>null</c>.
95-
/// </summary>
96-
public Func<JsonNode?, int, object?>? ArrayObjectItemKeyFinder { get; set; }
97-
98-
/// <summary>
99-
/// Gets or sets whether two instances of JSON object types (object and array)
100-
/// are considered equal if their position is the same in their parent
101-
/// arrays regardless of their contents. This property is only used when
102-
/// <see cref="ArrayItemMatcher"/> is set to <c>null</c>. By settings this
103-
/// property to <c>true</c>, a diff could be returned faster but larger in
104-
/// size. Default value is <c>false</c>.
105-
/// </summary>
106-
public bool ArrayObjectItemMatchByPosition { get; set; }
107-
108-
/// <summary>
109-
/// Gets or sets whether to prefer <see cref="ArrayObjectItemKeyFinder"/> and
110-
/// <see cref="ArrayObjectItemMatchByPosition"/> than using deep value comparison
111-
/// to match array object items. By settings this property to <c>true</c>,
112-
/// a diff could be returned faster but larger in size. Default value is <c>false</c>.
113-
/// </summary>
114-
public bool PreferFuzzyArrayItemMatch { get; set; }
115-
116-
/// <summary>
117-
/// Gets or sets the minimum length for diffing texts using <see cref="TextMatcher"/>
118-
/// or default text diffing algorithm, aka Google's diff-match-patch algorithm. When text
119-
/// diffing algorithm is not used, text diffing is fallback to value replacement. If this
120-
/// property is set to <c>0</c>, diffing algorithm is disabled. Default value is <c>0</c>.
121-
/// </summary>
122-
public int TextDiffMinLength { get; set; }
123-
124-
/// <summary>
125-
/// Gets or sets the function to match long texts.
126-
/// </summary>
127-
public TextMatch? TextMatcher { get; set; }
128-
}
129-
```
77+
Benchmarks 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/).
13078

131-
## Benchmark
79+
### Demo JSON object from `jsondiffpatch`
13280

13381
``` ini
13482

135-
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.631 (20H2/October2020Update)
136-
Intel Core i7-10750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
137-
.NET SDK=5.0.403
138-
[Host] : .NET 5.0.12 (5.0.1221.52207), X64 RyuJIT
139-
DefaultJob : .NET 5.0.12 (5.0.1221.52207), X64 RyuJIT
83+
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1415 (21H1/May2021Update)
84+
11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores
85+
.NET SDK=6.0.200
86+
[Host] : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT
87+
DefaultJob : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT
14088

14189

14290
```
143-
| Method | Mean | Min | Max | P95 | P80 | Allocated |
144-
|--------------------------- |------------:|------------:|------------:|------------:|------------:|----------:|
145-
| DemoObject_JsonNet | 165.4 μs | 158.4 μs | 168.6 μs | 168.0 μs | 167.4 μs | 173 KB |
146-
| DemoObject_DefaultOptions | 152.8 μs | 146.9 μs | 160.1 μs | 159.3 μs | 155.3 μs | 84 KB |
147-
| DemoObject_NoArrayMove | 152.0 μs | 147.4 μs | 154.0 μs | 153.6 μs | 153.2 μs | 84 KB |
148-
| DemoObject_Mutable | 104.1 μs | 101.6 μs | 106.1 μs | 106.0 μs | 105.1 μs | 70 KB |
149-
| LargeObject_JsonNet | 89,434.7 μs | 84,515.2 μs | 92,858.6 μs | 92,387.7 μs | 91,774.2 μs | 23,628 KB |
150-
| LargeObject_DefaultOptions | 7,446.1 μs | 7,156.2 μs | 7,794.0 μs | 7,775.8 μs | 7,518.6 μs | 4,085 KB |
151-
| LargeObject_NoArrayMove | 7,364.3 μs | 7,072.5 μs | 7,575.8 μs | 7,530.3 μs | 7,472.6 μs | 4,087 KB |
152-
| LargeObject_Mutable | 6,699.4 μs | 6,400.8 μs | 7,017.8 μs | 6,935.1 μs | 6,804.6 μs | 3,538 KB |
153-
154-
155-
_\* 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/SimpleDiffBenchmark.cs)_
91+
| Method | Mean | Min | Max | P95 | P80 | Allocated |
92+
|-------------------------- |----------:|----------:|----------:|----------:|----------:|----------:|
93+
| Diff_JsonNet | 107.55 μs | 95.29 μs | 122.73 μs | 118.01 μs | 113.43 μs | 132 KB |
94+
| Diff_SystemTextJson | 91.79 μs | 75.05 μs | 126.17 μs | 112.61 μs | 101.81 μs | 70 KB |
95+
| Diff_JsonNet_Rfc | 130.02 μs | 115.41 μs | 156.85 μs | 151.16 μs | 136.42 μs | 150 KB |
96+
| Diff_SystemTextJson_Rfc | 106.12 μs | 95.75 μs | 120.03 μs | 115.57 μs | 110.23 μs | 93 KB |
97+
| Patch_JsonNet | 116.92 μs | 107.06 μs | 137.86 μs | 133.26 μs | 122.34 μs | 162 KB |
98+
| Patch_SystemTextJson | 45.05 μs | 37.98 μs | 56.60 μs | 55.13 μs | 47.42 μs | 37 KB |
99+
| DeepEquals_JsonNet | 69.14 μs | 62.38 μs | 76.96 μs | 74.78 μs | 71.95 μs | 91 KB |
100+
| DeepEquals_SystemTextJson | 53.43 μs | 47.96 μs | 65.03 μs | 60.30 μs | 56.35 μs | 40 KB |
101+
| DeepClone_JsonNet | 51.28 μs | 44.65 μs | 62.34 μs | 58.69 μs | 54.52 μs | 70 KB |
102+
| DeepClone_SystemTextJson | 47.82 μs | 38.78 μs | 59.26 μs | 56.94 μs | 51.11 μs | 45 KB |
103+
104+
105+
### Large JSON object
106+
107+
``` ini
108+
109+
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1415 (21H1/May2021Update)
110+
11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores
111+
.NET SDK=6.0.200
112+
[Host] : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT
113+
DefaultJob : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT
114+
115+
116+
```
117+
| Method | Mean | Min | Max | P95 | P80 | Allocated |
118+
|-------------------------- |----------:|----------:|----------:|----------:|----------:|----------:|
119+
| Diff_JsonNet | 11.155 ms | 9.132 ms | 13.732 ms | 12.802 ms | 11.896 ms | 4 MB |
120+
| Diff_SystemTextJson | 8.375 ms | 7.334 ms | 9.132 ms | 8.978 ms | 8.690 ms | 3 MB |
121+
| Diff_JsonNet_Rfc | 12.774 ms | 9.807 ms | 17.990 ms | 17.479 ms | 13.374 ms | 6 MB |
122+
| Diff_SystemTextJson_Rfc | 11.664 ms | 10.341 ms | 12.918 ms | 12.668 ms | 12.142 ms | 5 MB |
123+
| Patch_JsonNet | 12.146 ms | 10.344 ms | 13.576 ms | 13.302 ms | 12.647 ms | 5 MB |
124+
| Patch_SystemTextJson | 4.693 ms | 2.422 ms | 5.685 ms | 5.412 ms | 5.061 ms | 2 MB |
125+
| DeepEquals_JsonNet | 4.164 ms | 2.584 ms | 5.424 ms | 4.917 ms | 4.652 ms | 2 MB |
126+
| DeepEquals_SystemTextJson | 3.254 ms | 2.071 ms | 3.837 ms | 3.709 ms | 3.499 ms | 2 MB |
127+
| DeepClone_JsonNet | 3.997 ms | 2.529 ms | 5.165 ms | 4.772 ms | 4.531 ms | 2 MB |
128+
| DeepClone_SystemTextJson | 3.396 ms | 2.085 ms | 3.981 ms | 3.811 ms | 3.609 ms | 2 MB |
129+

src/SystemTextJson.JsonDiffPatch/Diffs/DefaultArrayItemComparer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace System.Text.Json.JsonDiffPatch.Diffs
44
{
5-
internal readonly struct DefaultArrayItemComparer
5+
internal class DefaultArrayItemComparer
66
{
77
private readonly JsonDiffOptions _options;
88

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
using System.Linq;
2+
using System.Text.Json.Nodes;
3+
4+
namespace System.Text.Json.JsonDiffPatch.Diffs.Formatters
5+
{
6+
public abstract class DefaultDeltaFormatter<TResult> : IJsonDiffDeltaFormatter<TResult>
7+
{
8+
private readonly bool _usePatchableArrayChangeEnumerable;
9+
10+
protected DefaultDeltaFormatter()
11+
: this(false)
12+
{
13+
}
14+
15+
protected DefaultDeltaFormatter(bool usePatchableArrayChangeEnumerable)
16+
{
17+
_usePatchableArrayChangeEnumerable = usePatchableArrayChangeEnumerable;
18+
}
19+
20+
public virtual TResult? Format(ref JsonDiffDelta delta, JsonNode? left)
21+
{
22+
var value = CreateDefault();
23+
return FormatJsonDiffDelta(ref delta, left, value);
24+
}
25+
26+
protected virtual TResult? CreateDefault()
27+
{
28+
return default;
29+
}
30+
31+
protected virtual TResult? FormatJsonDiffDelta(ref JsonDiffDelta delta, JsonNode? left, TResult? existingValue)
32+
{
33+
switch (delta.Kind)
34+
{
35+
case DeltaKind.Added:
36+
existingValue = FormatAdded(ref delta, existingValue);
37+
break;
38+
case DeltaKind.Modified:
39+
existingValue = FormatModified(ref delta, left, existingValue);
40+
break;
41+
case DeltaKind.Deleted:
42+
existingValue = FormatDeleted(ref delta, left, existingValue);
43+
break;
44+
case DeltaKind.ArrayMove:
45+
existingValue = FormatArrayMove(ref delta, left, existingValue);
46+
break;
47+
case DeltaKind.Text:
48+
existingValue = FormatTextDiff(ref delta, CheckType<JsonValue>(left), existingValue);
49+
break;
50+
case DeltaKind.Array:
51+
existingValue = FormatArray(ref delta, CheckType<JsonArray>(left), existingValue);
52+
break;
53+
case DeltaKind.Object:
54+
existingValue = FormatObject(ref delta, CheckType<JsonObject>(left), existingValue);
55+
break;
56+
}
57+
58+
return existingValue;
59+
60+
static T CheckType<T>(JsonNode? node)
61+
{
62+
return node switch
63+
{
64+
T returnValue => returnValue,
65+
_ => throw new FormatException(JsonDiffDelta.InvalidPatchDocument)
66+
};
67+
}
68+
}
69+
70+
protected abstract TResult? FormatAdded(ref JsonDiffDelta delta, TResult? existingValue);
71+
protected abstract TResult? FormatModified(ref JsonDiffDelta delta, JsonNode? left, TResult? existingValue);
72+
protected abstract TResult? FormatDeleted(ref JsonDiffDelta delta, JsonNode? left, TResult? existingValue);
73+
protected abstract TResult? FormatArrayMove(ref JsonDiffDelta delta, JsonNode? left, TResult? existingValue);
74+
protected abstract TResult? FormatTextDiff(ref JsonDiffDelta delta, JsonValue? left, TResult? existingValue);
75+
76+
protected virtual TResult? FormatArray(ref JsonDiffDelta delta, JsonArray left, TResult? existingValue)
77+
{
78+
var arrayChangeEnumerable = _usePatchableArrayChangeEnumerable
79+
? delta.GetPatchableArrayChangeEnumerable(left, false)
80+
: delta.GetArrayChangeEnumerable();
81+
82+
return arrayChangeEnumerable
83+
.Aggregate(existingValue, (current, entry) =>
84+
{
85+
var elementDelta = entry.Diff;
86+
var leftValue = elementDelta.Kind switch
87+
{
88+
DeltaKind.Added or DeltaKind.None => null,
89+
_ => entry.Index < 0 || entry.Index >= left.Count
90+
? throw new FormatException(JsonDiffDelta.InvalidPatchDocument)
91+
: left[entry.Index]
92+
};
93+
return FormatArrayElement(entry, leftValue, current);
94+
});
95+
}
96+
97+
protected virtual TResult? FormatArrayElement(in JsonDiffDelta.ArrayChangeEntry arrayChange, JsonNode? left, TResult? existingValue)
98+
{
99+
var delta = arrayChange.Diff;
100+
return FormatJsonDiffDelta(ref delta, left, existingValue);
101+
}
102+
103+
protected virtual TResult? FormatObject(ref JsonDiffDelta delta, JsonObject left, TResult? existingValue)
104+
{
105+
var deltaDocument = delta.Document!.AsObject();
106+
foreach (var prop in deltaDocument)
107+
{
108+
var propDelta = new JsonDiffDelta(prop.Value!);
109+
left.TryGetPropertyValue(prop.Key, out var leftValue);
110+
existingValue = FormatObjectProperty(ref propDelta, leftValue, prop.Key, existingValue);
111+
}
112+
113+
return existingValue;
114+
}
115+
116+
protected virtual TResult? FormatObjectProperty(ref JsonDiffDelta delta, JsonNode? left, string propertyName, TResult? existingValue)
117+
{
118+
return FormatJsonDiffDelta(ref delta, left, existingValue);
119+
}
120+
}
121+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Text.Json.Nodes;
2+
3+
namespace System.Text.Json.JsonDiffPatch.Diffs.Formatters
4+
{
5+
/// <summary>
6+
/// Defines <see cref="JsonDiffDelta"/> formatting.
7+
/// </summary>
8+
public interface IJsonDiffDeltaFormatter<out TResult>
9+
{
10+
/// <summary>
11+
/// Creates a new JSON diff document from the <see cref="JsonDiffDelta"/>.
12+
/// </summary>
13+
/// <param name="delta">The JSON diff delta.</param>
14+
/// <param name="left">The left JSON object.</param>
15+
TResult? Format(ref JsonDiffDelta delta, JsonNode? left);
16+
}
17+
}
18+

0 commit comments

Comments
 (0)