Skip to content

Commit a0225fa

Browse files
authored
Init projects (#1)
Initialize projects and repo
1 parent 6522211 commit a0225fa

34 files changed

+11728
-1
lines changed

README.md

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,152 @@
1-
# system-text-json-jsondiffpath
1+
# system-text-json-jsondiffpath
2+
3+
High-performance, low-allocating JSON objects diff and patch extension for `System.Text.Json`.
4+
5+
## Features
6+
7+
- 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)
10+
- Fast large JSON document diffing with less memory consumption
11+
- 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+
- `JsonNode.Clone` and `JsonNode.DeepEquals` methods
14+
15+
- (_Under development_) Patch, unpatch, formatters etc
16+
17+
# Install
18+
19+
Install from [NuGet.org](https://www.nuget.org/packages/SystemTextJson.JsonDiffPatch/):
20+
21+
```
22+
Install-Package SystemTextJson.JsonDiffPatch
23+
```
24+
25+
## Usage
26+
### Diff
27+
28+
```csharp
29+
// Diff JSON files
30+
JsonDocument? diff = JsonDiffPatcher.DiffFile(file1, file2);
31+
// Diff Span<byte>
32+
JsonDocument? diff = JsonDiffPatcher.Diff(span1, span2);
33+
// Diff streams
34+
JsonDocument? diff = JsonDiffPatcher.Diff(stream1, stream2);
35+
// Diff JSON strings
36+
JsonDocument? diff = JsonDiffPatcher.Diff(json1, json2);
37+
// Diff JSON readers
38+
JsonDocument? diff = JsonDiffPatcher.Diff(ref reader1, ref reader2);
39+
40+
// Diff mutable JsonNode objects
41+
var node1 = JsonNode.Parse(...);
42+
var node2 = JsonNode.Parse(...);
43+
JsonNode? diff = node1.Diff(node2);
44+
```
45+
46+
### Clone
47+
48+
```csharp
49+
var node = JsonNode.Parse(...);
50+
JsonNode? cloned = node.Clone();
51+
```
52+
53+
### DeepEquals
54+
55+
```csharp
56+
var node1 = JsonNode.Parse(...);
57+
var node2 = JsonNode.Parse(...);
58+
bool equal = node1.DeepEquals(node2);
59+
```
60+
61+
### Options
62+
63+
```csharp
64+
public struct JsonDiffOptions
65+
{
66+
/// <summary>
67+
/// Specifies whether to suppress detect array move. Default value is <c>false</c>.
68+
/// </summary>
69+
public bool SuppressDetectArrayMove { get; set; }
70+
71+
/// <summary>
72+
/// Specifies whether to include moved item value. Default value is <c>false</c>.
73+
/// </summary>
74+
/// <remarks>
75+
/// See <see link="https://github.com/benjamine/jsondiffpatch/blob/master/docs/deltas.md#array-moves"/>.
76+
/// </remarks>
77+
public bool IncludeValueOnMove { get; set; }
78+
79+
/// <summary>
80+
/// Gets or sets the function to match array items.
81+
/// </summary>
82+
public ArrayItemMatch? ArrayItemMatcher { get; set; }
83+
84+
/// <summary>
85+
/// Gets or sets the function to find key of a <see cref="JsonObject"/>
86+
/// or <see cref="JsonArray"/>. This is used when matching array items by
87+
/// their keys. If this function returns <c>null</c>, the items being
88+
/// compared are treated as "not keyed". When comparing two "not keyed"
89+
/// objects, their contents are compared. This function is only used when
90+
/// <see cref="ArrayItemMatcher"/> is set to <c>null</c>.
91+
/// </summary>
92+
public Func<JsonNode?, int, object?>? ArrayObjectItemKeyFinder { get; set; }
93+
94+
/// <summary>
95+
/// Gets or sets whether two instances of JSON object types (object and array)
96+
/// are considered equal if their position is the same in their parent
97+
/// arrays regardless of their contents. This property is only used when
98+
/// <see cref="ArrayItemMatcher"/> is set to <c>null</c>. By settings this
99+
/// property to <c>true</c>, a diff could be returned faster but larger in
100+
/// size. Default value is <c>false</c>.
101+
/// </summary>
102+
public bool ArrayObjectItemMatchByPosition { get; set; }
103+
104+
/// <summary>
105+
/// Gets or sets whether to prefer <see cref="ArrayObjectItemKeyFinder"/> and
106+
/// <see cref="ArrayObjectItemMatchByPosition"/> than using deep value comparison
107+
/// to match array object items. By settings this property to <c>true</c>,
108+
/// a diff could be returned faster but larger in size. Default value is <c>false</c>.
109+
/// </summary>
110+
public bool PreferFuzzyArrayItemMatch { get; set; }
111+
112+
/// <summary>
113+
/// Gets or sets the minimum length for diffing texts using <see cref="TextMatcher"/>
114+
/// or default text diffing algorithm, aka Google's diff-match-patch algorithm. When text
115+
/// diffing algorithm is not used, text diffing is fallback to value replacement. If this
116+
/// property is set to <c>0</c>, diffing algorithm is disabled. Default value is <c>0</c>.
117+
/// </summary>
118+
public int TextDiffMinLength { get; set; }
119+
120+
/// <summary>
121+
/// Gets or sets the function to match long texts.
122+
/// </summary>
123+
public TextMatch? TextMatcher { get; set; }
124+
}
125+
```
126+
127+
## Benchmark
128+
129+
``` ini
130+
131+
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.631 (20H2/October2020Update)
132+
Intel Core i7-10750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
133+
.NET SDK=5.0.403
134+
[Host] : .NET 5.0.12 (5.0.1221.52207), X64 RyuJIT
135+
DefaultJob : .NET 5.0.12 (5.0.1221.52207), X64 RyuJIT
136+
137+
138+
```
139+
| Method | Mean | Min | Max | P95 | P80 | Allocated |
140+
|--------------------------- |------------:|------------:|------------:|------------:|------------:|----------:|
141+
| DemoObject_JsonNet | 165.4 μs | 158.4 μs | 168.6 μs | 168.0 μs | 167.4 μs | 173 KB |
142+
| DemoObject_DefaultOptions | 152.8 μs | 146.9 μs | 160.1 μs | 159.3 μs | 155.3 μs | 84 KB |
143+
| DemoObject_NoArrayMove | 152.0 μs | 147.4 μs | 154.0 μs | 153.6 μs | 153.2 μs | 84 KB |
144+
| DemoObject_Mutable | 104.1 μs | 101.6 μs | 106.1 μs | 106.0 μs | 105.1 μs | 70 KB |
145+
| 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 |
146+
| 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 |
147+
| 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 |
148+
| 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 |
149+
150+
151+
_\* Generated using example objects [here](https://github.com/weichch/system-text-json-jsondiffpath/tree/main/test/Examples) and benchmark tests [here](https://github.com/weichch/system-text-json-jsondiffpath/tree/main/test/SystemTextJson.JsonDiffPatch.Benchmark/SimpleDiffBenchmark.cs)_
152+

icon.png

2.2 KB
Loading

src/SystemTextJson.JsonDiffPatch.sln

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 16
4+
VisualStudioVersion = 16.0.31829.152
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SystemTextJson.JsonDiffPatch", "SystemTextJson.JsonDiffPatch\SystemTextJson.JsonDiffPatch.csproj", "{2F8D59B0-53DE-4EE7-A704-D3F89A00D9DC}"
7+
EndProject
8+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{AE524BA2-D5E2-4D5F-BC95-9AA064D46234}"
9+
EndProject
10+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SystemTextJson.JsonDiffPatch.UnitTests", "..\test\SystemTextJson.JsonDiffPatch.UnitTests\SystemTextJson.JsonDiffPatch.UnitTests.csproj", "{832E60B1-0225-48B3-9AAD-638A0F5AA15E}"
11+
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SystemTextJson.JsonDiffPatch.Benchmark", "..\test\SystemTextJson.JsonDiffPatch.Benchmark\SystemTextJson.JsonDiffPatch.Benchmark.csproj", "{0F173CC9-EA04-4243-A506-E2C59C905A5E}"
13+
EndProject
14+
Global
15+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
16+
Debug|Any CPU = Debug|Any CPU
17+
Release|Any CPU = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
20+
{2F8D59B0-53DE-4EE7-A704-D3F89A00D9DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{2F8D59B0-53DE-4EE7-A704-D3F89A00D9DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{2F8D59B0-53DE-4EE7-A704-D3F89A00D9DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{2F8D59B0-53DE-4EE7-A704-D3F89A00D9DC}.Release|Any CPU.Build.0 = Release|Any CPU
24+
{832E60B1-0225-48B3-9AAD-638A0F5AA15E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25+
{832E60B1-0225-48B3-9AAD-638A0F5AA15E}.Debug|Any CPU.Build.0 = Debug|Any CPU
26+
{832E60B1-0225-48B3-9AAD-638A0F5AA15E}.Release|Any CPU.ActiveCfg = Release|Any CPU
27+
{832E60B1-0225-48B3-9AAD-638A0F5AA15E}.Release|Any CPU.Build.0 = Release|Any CPU
28+
{0F173CC9-EA04-4243-A506-E2C59C905A5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29+
{0F173CC9-EA04-4243-A506-E2C59C905A5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
30+
{0F173CC9-EA04-4243-A506-E2C59C905A5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
31+
{0F173CC9-EA04-4243-A506-E2C59C905A5E}.Release|Any CPU.Build.0 = Release|Any CPU
32+
EndGlobalSection
33+
GlobalSection(SolutionProperties) = preSolution
34+
HideSolutionNode = FALSE
35+
EndGlobalSection
36+
GlobalSection(NestedProjects) = preSolution
37+
{832E60B1-0225-48B3-9AAD-638A0F5AA15E} = {AE524BA2-D5E2-4D5F-BC95-9AA064D46234}
38+
{0F173CC9-EA04-4243-A506-E2C59C905A5E} = {AE524BA2-D5E2-4D5F-BC95-9AA064D46234}
39+
EndGlobalSection
40+
GlobalSection(ExtensibilityGlobals) = postSolution
41+
SolutionGuid = {F15DC234-2EF5-47F9-BB66-3C651311A3E5}
42+
EndGlobalSection
43+
EndGlobal
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.Text.Json.Nodes;
2+
3+
namespace System.Text.Json.Diffs
4+
{
5+
/// <summary>
6+
/// Defines a function that determines whether two items in arrays are equal.
7+
/// </summary>
8+
/// <param name="x">The element in array1.</param>
9+
/// <param name="indexX">The index of <paramref name="x"/> in array1.</param>
10+
/// <param name="y">The element in array2.</param>
11+
/// <param name="indexY">The index of <paramref name="y"/> in array2.</param>
12+
public delegate bool ArrayItemMatch(JsonNode? x, int indexX,
13+
JsonNode? y, int indexY, out bool deepEqual);
14+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System.Text.Json.Nodes;
2+
3+
namespace System.Text.Json.Diffs
4+
{
5+
internal readonly struct DefaultArrayItemComparer
6+
{
7+
private readonly JsonDiffOptionsView _options;
8+
9+
public DefaultArrayItemComparer(in JsonDiffOptionsView options)
10+
{
11+
_options = options;
12+
}
13+
14+
public bool MatchArrayItem(
15+
JsonNode? x, int indexX, JsonNode? y, int indexY,
16+
out bool deepEqual)
17+
{
18+
// Returns if one JSON node matches another. Nodes are considered match if:
19+
// - they are deeply equal, OR
20+
// - they are of JavaScript object type JsonObject and JsonArray and their
21+
// positions in corresponding arrays are equal
22+
// See: https://github.com/benjamine/jsondiffpatch/blob/a8cde4c666a8a25d09d8f216c7f19397f2e1b569/src/filters/arrays.js#L43
23+
24+
deepEqual = false;
25+
26+
if (_options.PreferFuzzyArrayItemMatch
27+
&& x is JsonObject or JsonArray
28+
&& y is JsonObject or JsonArray)
29+
{
30+
if (FuzzyMatchItem(x, indexX, y, indexY,
31+
out var fuzzyResult, out deepEqual))
32+
{
33+
return fuzzyResult;
34+
}
35+
}
36+
37+
if (x.DeepEquals(y))
38+
{
39+
deepEqual = true;
40+
return true;
41+
}
42+
43+
if (!_options.PreferFuzzyArrayItemMatch
44+
&& x is JsonObject or JsonArray
45+
&& y is JsonObject or JsonArray)
46+
{
47+
if (FuzzyMatchItem(x, indexX, y, indexY,
48+
out var fuzzyResult, out deepEqual))
49+
{
50+
return fuzzyResult;
51+
}
52+
}
53+
54+
return false;
55+
}
56+
57+
private bool FuzzyMatchItem(JsonNode? x, int indexX,
58+
JsonNode? y, int indexY,
59+
out bool result, out bool deepEqual)
60+
{
61+
result = false;
62+
deepEqual = false;
63+
64+
var keyFinder = _options.ArrayObjectItemKeyFinder;
65+
if (keyFinder is not null)
66+
{
67+
var keyX = keyFinder(x, indexX);
68+
var keyY = keyFinder(y, indexY);
69+
70+
if (keyX is null && keyY is null)
71+
{
72+
// Use DeepEquals if both items are not keyed
73+
return false;
74+
}
75+
76+
result = Equals(keyX, keyY);
77+
return true;
78+
}
79+
80+
if (_options.ArrayObjectItemMatchByPosition)
81+
{
82+
if (indexX == indexY)
83+
{
84+
result = true;
85+
return true;
86+
}
87+
88+
// We don't return a result for objects at different position
89+
// so that we could still compare them using DeepEquals, or
90+
// return "not equal" if this method is called after.
91+
}
92+
93+
return false;
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)