├── .github ├── CODEOWNERS └── workflows │ └── build-and-test.yaml ├── icon.png ├── src ├── SystemTextJson.JsonDiffPatch │ ├── JsonStringValueKind.cs │ ├── Properties │ │ ├── AssemblyInfo.cs │ │ └── IsExternalInit.cs │ ├── Diffs │ │ ├── ArrayItemMatch.cs │ │ ├── TextMatch.cs │ │ ├── Formatters │ │ │ ├── IJsonDiffDeltaFormatter.cs │ │ │ ├── DefaultDeltaFormatter.cs │ │ │ └── JsonPatchDeltaFormatter.cs │ │ ├── JsonDiffContext.cs │ │ ├── ArrayItemMatchContext.cs │ │ ├── JsonDiffPatcher.Object.cs │ │ ├── JsonDiffOptions.cs │ │ ├── JsonDiffPatcher.Text.cs │ │ └── Lcs.cs │ ├── Patching │ │ ├── TextPatch.cs │ │ ├── JsonPatchOptions.cs │ │ ├── JsonReversePatchOptions.cs │ │ ├── JsonDiffPatcher.Text.cs │ │ ├── JsonDiffPatcher.Array.cs │ │ ├── JsonDiffPatcher.Object.cs │ │ └── JsonDiffPatcher.Patch.cs │ ├── JsonElementComparison.cs │ ├── JsonDiffPatcher.cs │ ├── JsonComparerOptions.cs │ ├── JsonValueComparer.String.cs │ ├── JsonValueComparer.Number.cs │ ├── SystemTextJson.JsonDiffPatch.csproj │ ├── JsonValueComparer.cs │ ├── JsonBytes.cs │ └── JsonValueWrapper.cs ├── SystemTextJson.JsonDiffPatch.NUnit │ ├── JsonEqualConstraint.cs │ ├── JsonNotEqualConstraint.cs │ ├── SystemTextJson.JsonDiffPatch.NUnit.csproj │ ├── JsonDiffConstraintResult.cs │ ├── JsonIs.cs │ └── JsonDiffConstraint.cs ├── SystemTextJson.JsonDiffPatch.Xunit │ ├── JsonNotEqualException.cs │ ├── JsonEqualException.cs │ └── SystemTextJson.JsonDiffPatch.Xunit.csproj ├── SystemTextJson.JsonDiffPatch.MSTest │ └── SystemTextJson.JsonDiffPatch.MSTest.csproj ├── Directory.Build.props └── SystemTextJson.JsonDiffPatch.sln ├── test ├── Directory.Build.props ├── SystemTextJson.JsonDiffPatch.UnitTests │ ├── DocumentTests │ │ ├── DocumentTestData.cs │ │ └── DeepEqualsTests.cs │ ├── ElementTests │ │ ├── ElementTestData.cs │ │ └── DeepEqualsTests.cs │ ├── DefaultOptionsTests.cs │ ├── SystemTextJson.JsonDiffPatch.UnitTests.csproj │ ├── NodeTests │ │ ├── JsonValueComparerTests.cs │ │ ├── ObjectDeepEqualsTests.cs │ │ ├── PatchTests.cs │ │ ├── DiffTests.cs │ │ └── ElementDeepEqualsTests.cs │ ├── DeepEqualsTestData.cs │ ├── DemoFileTests.cs │ └── FormatterTests │ │ └── JsonPatchDeltaFormatterTests.cs ├── SystemTextJson.JsonDiffPatch.NUnit.Tests │ ├── SystemTextJson.JsonDiffPatch.NUnit.Tests.csproj │ └── JsonAssertTests.cs ├── SystemTextJson.JsonDiffPatch.MSTest.Tests │ ├── SystemTextJson.JsonDiffPatch.MSTest.Tests.csproj │ └── JsonAssertTests.cs ├── SystemTextJson.JsonDiffPatch.Benchmark │ ├── PatchJsonFileBenchmark.cs │ ├── QuickDiff.cs │ ├── OptionsJsonFileBenchmark.cs │ ├── BasicBenchmark.cs │ ├── JsonFileBenchmark.cs │ ├── DeepEqualsJsonFileBenchmark.cs │ ├── Program.cs │ ├── BenchmarkHelper.cs │ ├── QuickDeepEquals.cs │ ├── DiffJsonFileBenchmark.cs │ ├── SystemTextJson.JsonDiffPatch.Benchmark.csproj │ └── JsonHelper.cs ├── SystemTextJson.JsonDiffPatch.Xunit.Tests │ ├── SystemTextJson.JsonDiffPatch.Xunit.Tests.csproj │ └── JsonAssertTests.cs └── Examples │ ├── demo_diff.json │ ├── demo_diff_jsonpatch.json │ ├── demo_right.json │ ├── demo_left.json │ └── demo_diff_notext.json ├── LICENSE ├── ReleaseNotes.md ├── Benchmark.md ├── README.md └── .gitignore /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @weichch -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weichch/system-text-json-jsondiffpatch/HEAD/icon.png -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/JsonStringValueKind.cs: -------------------------------------------------------------------------------- 1 | namespace System.Text.Json.JsonDiffPatch 2 | { 3 | internal enum JsonStringValueKind 4 | { 5 | String, 6 | DateTime, 7 | Guid 8 | } 9 | } -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly:InternalsVisibleTo("SystemTextJson.JsonDiffPatch.Benchmark")] 4 | [assembly:InternalsVisibleTo("SystemTextJson.JsonDiffPatch.UnitTests")] 5 | -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net7.0;net6.0;net48 5 | 6 | 7 | 8 | false 9 | enable 10 | latest 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Diffs/ArrayItemMatch.cs: -------------------------------------------------------------------------------- 1 | namespace System.Text.Json.JsonDiffPatch.Diffs 2 | { 3 | /// 4 | /// Defines a function that determines whether two items in arrays are equal. 5 | /// 6 | /// The comparison context. 7 | public delegate bool ArrayItemMatch(ref ArrayItemMatchContext context); 8 | } 9 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Diffs/TextMatch.cs: -------------------------------------------------------------------------------- 1 | namespace System.Text.Json.JsonDiffPatch.Diffs 2 | { 3 | /// 4 | /// Defines a function that diffs two long texts. 5 | /// 6 | /// The left string. 7 | /// The right string. 8 | public delegate string? TextMatch(string str1, string str2); 9 | } 10 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Patching/TextPatch.cs: -------------------------------------------------------------------------------- 1 | namespace System.Text.Json.JsonDiffPatch.Patching 2 | { 3 | /// 4 | /// Defines a function that diffs two long texts. 5 | /// 6 | /// The left string. 7 | /// The right string. 8 | public delegate string TextPatch(string str1, string str2); 9 | } 10 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch.NUnit/JsonEqualConstraint.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | 3 | namespace System.Text.Json.JsonDiffPatch.Nunit 4 | { 5 | class JsonEqualConstraint : JsonDiffConstraint 6 | { 7 | public JsonEqualConstraint(JsonNode? expected) 8 | : base(expected) 9 | { 10 | } 11 | 12 | protected override bool Test() => Delta is null; 13 | } 14 | } -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Properties/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/64749385/predefined-type-system-runtime-compilerservices-isexternalinit-is-not-defined 2 | // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives 3 | #if !NET 4 | namespace System.Runtime.CompilerServices 5 | { 6 | internal static class IsExternalInit 7 | { 8 | } 9 | } 10 | #endif -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch.Xunit/JsonNotEqualException.cs: -------------------------------------------------------------------------------- 1 | using Xunit.Sdk; 2 | 3 | namespace System.Text.Json.JsonDiffPatch.Xunit 4 | { 5 | /// 6 | /// Exception thrown when two JSON objects are unexpectedly equal. 7 | /// 8 | public class JsonNotEqualException : XunitException 9 | { 10 | public JsonNotEqualException() 11 | : base("JsonAssert.NotEqual() failure.") 12 | { 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch.NUnit/JsonNotEqualConstraint.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | 3 | namespace System.Text.Json.JsonDiffPatch.Nunit 4 | { 5 | class JsonNotEqualConstraint : JsonDiffConstraint 6 | { 7 | public JsonNotEqualConstraint(JsonNode? expected) 8 | : base(expected) 9 | { 10 | } 11 | 12 | public override Func? OutputFormatter => null; 13 | protected override bool Test() => Delta is not null; 14 | } 15 | } -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Patching/JsonPatchOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch.Patching; 2 | 3 | namespace System.Text.Json.JsonDiffPatch 4 | { 5 | /// 6 | /// Represents options for patching JSON object. 7 | /// 8 | public struct JsonPatchOptions 9 | { 10 | /// 11 | /// Gets or sets the function to patch long text. 12 | /// 13 | public TextPatch? TextPatchProvider { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Patching/JsonReversePatchOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch.Patching; 2 | 3 | namespace System.Text.Json.JsonDiffPatch 4 | { 5 | /// 6 | /// Represents options for patching JSON object. 7 | /// 8 | public struct JsonReversePatchOptions 9 | { 10 | /// 11 | /// Gets or sets the function to reverse long text patch. 12 | /// 13 | public TextPatch? ReverseTextPatchProvider { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch.Xunit/JsonEqualException.cs: -------------------------------------------------------------------------------- 1 | using Xunit.Sdk; 2 | 3 | namespace System.Text.Json.JsonDiffPatch.Xunit 4 | { 5 | /// 6 | /// Exception thrown when two JSON objects are unexpectedly not equal. 7 | /// 8 | public class JsonEqualException : XunitException 9 | { 10 | public JsonEqualException() 11 | : base("JsonAssert.Equal() failure.") 12 | { 13 | } 14 | 15 | public JsonEqualException(string output) 16 | : base($"JsonAssert.Equal() failure.{Environment.NewLine}{output}") 17 | { 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.UnitTests/DocumentTests/DocumentTestData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json; 3 | 4 | namespace SystemTextJson.JsonDiffPatch.UnitTests.DocumentTests 5 | { 6 | public class DocumentTestData 7 | { 8 | public static IEnumerable RawTextEqual => DeepEqualsTestData.RawTextEqual(Json); 9 | 10 | public static IEnumerable SemanticEqual => DeepEqualsTestData.SemanticEqual(Json); 11 | 12 | private static JsonDocument? Json(string? jsonValue) 13 | { 14 | return jsonValue is null ? null : JsonDocument.Parse($"{jsonValue}"); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/JsonElementComparison.cs: -------------------------------------------------------------------------------- 1 | namespace System.Text.Json.JsonDiffPatch 2 | { 3 | /// 4 | /// Represents comparison modes. 5 | /// 6 | public enum JsonElementComparison 7 | { 8 | /// 9 | /// Only compares raw text of two instances. 10 | /// 11 | RawText, 12 | 13 | /// 14 | /// Deserializes both instances into value object of the most significant type 15 | /// and compares the value objects. 16 | /// 17 | Semantic 18 | } 19 | } -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Diffs/Formatters/IJsonDiffDeltaFormatter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | 3 | namespace System.Text.Json.JsonDiffPatch.Diffs.Formatters 4 | { 5 | /// 6 | /// Defines formatting. 7 | /// 8 | public interface IJsonDiffDeltaFormatter 9 | { 10 | /// 11 | /// Creates a new JSON diff document from the . 12 | /// 13 | /// The JSON diff delta. 14 | /// The left JSON object. 15 | TResult? Format(ref JsonDiffDelta delta, JsonNode? left); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/JsonDiffPatcher.cs: -------------------------------------------------------------------------------- 1 | namespace System.Text.Json.JsonDiffPatch 2 | { 3 | /// 4 | /// Provides methods to diff and patch JSON objects. 5 | /// 6 | public static partial class JsonDiffPatcher 7 | { 8 | /// 9 | /// Gets or sets the default diff options. 10 | /// 11 | public static Func? DefaultOptions { get; set; } 12 | 13 | /// 14 | /// Gets or sets the default comparison mode used by DeepEquals to compare . 15 | /// 16 | public static JsonElementComparison DefaultComparison { get; set; } = JsonElementComparison.RawText; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch.NUnit/SystemTextJson.JsonDiffPatch.NUnit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | System.Text.Json.JsonDiffPatch.Nunit 5 | 6 | 7 | 8 | SystemTextJson.JsonDiffPatch.NUnit 9 | nunit;assert;compare;json;system-text-json 10 | Extension methods for NUnit to compare JSON in unit tests. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.UnitTests/ElementTests/ElementTestData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json; 3 | 4 | namespace SystemTextJson.JsonDiffPatch.UnitTests.ElementTests 5 | { 6 | public class ElementTestData 7 | { 8 | public static IEnumerable RawTextEqual => DeepEqualsTestData.RawTextEqual(Json); 9 | 10 | public static IEnumerable SemanticEqual => DeepEqualsTestData.SemanticEqual(Json); 11 | 12 | private static object? Json(string? jsonValue) 13 | { 14 | // ReSharper disable once HeapView.BoxingAllocation 15 | return jsonValue is null 16 | ? default 17 | : JsonSerializer.Deserialize($"{jsonValue}"); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch.Xunit/SystemTextJson.JsonDiffPatch.Xunit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | System.Text.Json.JsonDiffPatch.Xunit 5 | 6 | 7 | 8 | SystemTextJson.JsonDiffPatch.Xunit 9 | xunit;assert;compare;json;system-text-json 10 | Extension methods for XUnit to compare JSON in unit tests. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.NUnit.Tests/SystemTextJson.JsonDiffPatch.NUnit.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | runtime; build; native; contentfiles; analyzers; buildtransitive 9 | all 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch.MSTest/SystemTextJson.JsonDiffPatch.MSTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | System.Text.Json.JsonDiffPatch.MsTest 5 | 6 | 7 | 8 | SystemTextJson.JsonDiffPatch.MSTest 9 | mstest;mstest2;assert;compare;json;system-text-json 10 | Extension methods for MSTest v2 to compare JSON in unit tests. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.MSTest.Tests/SystemTextJson.JsonDiffPatch.MSTest.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | runtime; build; native; contentfiles; analyzers; buildtransitive 9 | all 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/JsonComparerOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace System.Text.Json.JsonDiffPatch 5 | { 6 | internal readonly struct JsonComparerOptions 7 | { 8 | private readonly JsonElementComparison? _jsonElementComparison; 9 | 10 | public JsonComparerOptions(JsonElementComparison? jsonElementComparison, 11 | IEqualityComparer? valueComparer) 12 | { 13 | _jsonElementComparison = jsonElementComparison; 14 | ValueComparer = valueComparer; 15 | } 16 | 17 | public JsonElementComparison JsonElementComparison 18 | => _jsonElementComparison ?? JsonDiffPatcher.DefaultComparison; 19 | 20 | public IEqualityComparer? ValueComparer { get; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch.NUnit/JsonDiffConstraintResult.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework.Constraints; 2 | 3 | namespace System.Text.Json.JsonDiffPatch.Nunit 4 | { 5 | class JsonDiffConstraintResult : ConstraintResult 6 | { 7 | private readonly JsonDiffConstraint _constraint; 8 | 9 | public JsonDiffConstraintResult(JsonDiffConstraint constraint, object actual, bool isSuccess) 10 | : base(constraint, actual, isSuccess) 11 | { 12 | _constraint = constraint; 13 | } 14 | 15 | public override void WriteMessageTo(MessageWriter writer) 16 | { 17 | if (_constraint.OutputFormatter is null || _constraint.Delta is null) 18 | { 19 | return; 20 | } 21 | 22 | writer.Write(_constraint.OutputFormatter(_constraint.Delta)); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/JsonValueComparer.String.cs: -------------------------------------------------------------------------------- 1 | namespace System.Text.Json.JsonDiffPatch 2 | { 3 | static partial class JsonValueComparer 4 | { 5 | internal static int CompareByteArray(byte[] x, byte[] y) 6 | { 7 | if (x.Length == 0 && y.Length == 0) 8 | { 9 | return 0; 10 | } 11 | 12 | var lengthCompare = x.Length.CompareTo(y.Length); 13 | if (lengthCompare != 0) 14 | { 15 | return lengthCompare; 16 | } 17 | 18 | for (var i = 0; i < x.Length; i++) 19 | { 20 | var valueCompare = x[i].CompareTo(y[i]); 21 | if (valueCompare != 0) 22 | { 23 | return valueCompare; 24 | } 25 | } 26 | 27 | return 0; 28 | } 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch.NUnit/JsonIs.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | 3 | namespace System.Text.Json.JsonDiffPatch.Nunit 4 | { 5 | /// 6 | /// Provides methods to create JSON assert constraints. 7 | /// 8 | public static class JsonIs 9 | { 10 | /// 11 | /// Returns a constraint that tests whether two JSON are equal. 12 | /// 13 | /// The expected value. 14 | public static JsonDiffConstraint EqualTo(JsonNode? expected) => new JsonEqualConstraint(expected); 15 | 16 | /// 17 | /// Returns a constraint that tests whether two JSON are not equal. 18 | /// 19 | /// The expected value. 20 | public static JsonDiffConstraint NotEqualTo(JsonNode? expected) => new JsonNotEqualConstraint(expected); 21 | } 22 | } -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.Benchmark/PatchJsonFileBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch; 2 | using System.Text.Json.Nodes; 3 | using BenchmarkDotNet.Attributes; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace SystemTextJson.JsonDiffPatch.Benchmark 7 | { 8 | public class PatchJsonFileBenchmark : JsonFileBenchmark 9 | { 10 | [Benchmark] 11 | public JsonNode SystemTextJson() 12 | { 13 | var node1 = JsonNode.Parse(JsonLeft); 14 | var diff = JsonNode.Parse(JsonDiff); 15 | 16 | JsonDiffPatcher.Patch(ref node1, diff); 17 | return node1!; 18 | } 19 | 20 | [Benchmark] 21 | public JToken JsonNet() 22 | { 23 | var token1 = JToken.Parse(JsonLeft); 24 | var diff = JToken.Parse(JsonDiff); 25 | 26 | return BenchmarkHelper.CreateJsonNetDiffPatch().Patch(token1, diff); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.Xunit.Tests/SystemTextJson.JsonDiffPatch.Xunit.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | runtime; build; native; contentfiles; analyzers; buildtransitive 8 | all 9 | 10 | 11 | runtime; build; native; contentfiles; analyzers; buildtransitive 12 | all 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/JsonValueComparer.Number.cs: -------------------------------------------------------------------------------- 1 | namespace System.Text.Json.JsonDiffPatch 2 | { 3 | static partial class JsonValueComparer 4 | { 5 | private static bool AreDoubleClose(double x, double y) 6 | { 7 | // https://stackoverflow.com/questions/3874627/floating-point-comparison-functions-for-c-sharp 8 | const double epsilon = 2.22044604925031E-16; 9 | 10 | // ReSharper disable once CompareOfFloatsByEqualityOperator 11 | if (x == y) 12 | return true; 13 | 14 | var tolerance = (Math.Abs(x) + Math.Abs(y) + 10.0) * epsilon; 15 | var difference = x - y; 16 | 17 | if (-tolerance < difference) 18 | return tolerance > difference; 19 | 20 | return false; 21 | } 22 | 23 | internal static int CompareDouble(double x, double y) 24 | { 25 | return AreDoubleClose(x, y) ? 0 : x.CompareTo(y); 26 | } 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/SystemTextJson.JsonDiffPatch.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | System.Text.Json.JsonDiffPatch 5 | 6 | 7 | 8 | SystemTextJson.JsonDiffPatch 9 | json;diff;compare;patch;system-text-json;jsondiffpatch 10 | High-performance, low-allocating JSON object diff and patch extension for System.Text.Json. Support generating patch document in RFC 6902 JSON Patch format. Provides bonus DeepEquals and DeepClone methods. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.Benchmark/QuickDiff.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch; 2 | using System.Text.Json.Nodes; 3 | using BenchmarkDotNet.Attributes; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace SystemTextJson.JsonDiffPatch.Benchmark 7 | { 8 | [IterationCount(10)] 9 | public class QuickDiff : JsonFileBenchmark 10 | { 11 | [Params(JsonFileSize.Small)] 12 | public override JsonFileSize FileSize { get; set; } 13 | 14 | [Benchmark] 15 | public JsonNode? SystemTextJson() 16 | { 17 | var node1 = JsonNode.Parse(JsonLeft); 18 | var node2 = JsonNode.Parse(JsonRight); 19 | 20 | return node1.Diff(node2, BenchmarkHelper.CreateDiffOptionsWithJsonNetMatch()); 21 | } 22 | 23 | [Benchmark] 24 | public JToken JsonNet() 25 | { 26 | var token1 = JToken.Parse(JsonLeft); 27 | var token2 = JToken.Parse(JsonRight); 28 | 29 | return BenchmarkHelper.CreateJsonNetDiffPatch().Diff(token1, token2); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/JsonValueComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | 3 | namespace System.Text.Json.JsonDiffPatch 4 | { 5 | /// 6 | /// Comparer for . 7 | /// 8 | public static partial class JsonValueComparer 9 | { 10 | /// 11 | /// Compares two . 12 | /// 13 | /// The left value. 14 | /// The right value. 15 | public static int Compare(JsonValue? x, JsonValue? y) 16 | { 17 | if (x is null && y is null) 18 | { 19 | return 0; 20 | } 21 | 22 | if (x is null) 23 | { 24 | return -1; 25 | } 26 | 27 | if (y is null) 28 | { 29 | return 1; 30 | } 31 | 32 | var wrapperX = new JsonValueWrapper(x); 33 | var wrapperY = new JsonValueWrapper(y); 34 | 35 | return wrapperX.CompareTo(ref wrapperY); 36 | } 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Wei Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yaml: -------------------------------------------------------------------------------- 1 | name: build-and-test 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | env: 8 | JsonDiffPatchSolutionPath: src/SystemTextJson.JsonDiffPatch.sln 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | build-configuration: [ Debug, Release ] 14 | test-target-framework: [ net8.0, net7.0, net6.0 ] 15 | name: Build And Test (${{ matrix.test-target-framework }}, ${{ matrix.build-configuration }}) 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup DotNet 20 | uses: actions/setup-dotnet@v2 21 | with: 22 | dotnet-version: | 23 | 8.x 24 | 7.x 25 | 6.x 26 | - name: Restore 27 | run: dotnet restore ${{ env.JsonDiffPatchSolutionPath }} 28 | - name: Build 29 | run: dotnet build -c ${{ matrix.build-configuration }} --no-restore ${{ env.JsonDiffPatchSolutionPath }} 30 | - name: Test 31 | run: dotnet test -c ${{ matrix.build-configuration }} -f ${{ matrix.test-target-framework }} --no-restore --no-build ${{ env.JsonDiffPatchSolutionPath }} 32 | 33 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.Benchmark/OptionsJsonFileBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch; 2 | using System.Text.Json.Nodes; 3 | using BenchmarkDotNet.Attributes; 4 | 5 | namespace SystemTextJson.JsonDiffPatch.Benchmark 6 | { 7 | public class OptionsJsonFileBenchmark : JsonFileBenchmark 8 | { 9 | [Params(JsonFileSize.Small)] 10 | public override JsonFileSize FileSize { get; set; } 11 | 12 | [Benchmark] 13 | public JsonNode RawText() 14 | { 15 | var node1 = JsonNode.Parse(JsonLeft); 16 | var node2 = JsonNode.Parse(JsonRight); 17 | 18 | return node1.Diff(node2, new JsonDiffOptions 19 | { 20 | JsonElementComparison = JsonElementComparison.RawText 21 | })!; 22 | } 23 | 24 | [Benchmark] 25 | public JsonNode Semantic() 26 | { 27 | var node1 = JsonNode.Parse(JsonLeft); 28 | var node2 = JsonNode.Parse(JsonRight); 29 | 30 | return node1.Diff(node2, new JsonDiffOptions 31 | { 32 | JsonElementComparison = JsonElementComparison.Semantic 33 | })!; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.Benchmark/BasicBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch; 2 | using System.Text.Json.Nodes; 3 | using BenchmarkDotNet.Attributes; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace SystemTextJson.JsonDiffPatch.Benchmark 7 | { 8 | [IterationCount(50)] 9 | public class BasicBenchmark 10 | { 11 | [Benchmark] 12 | public JsonNode Lcs_SystemTextJson() 13 | { 14 | var nodeX = JsonNode.Parse("[1,2,3,0,1,2,3,4,5,6,7,8,9,10,1,2,3]"); 15 | var nodeY = JsonNode.Parse("[1,2,3,10,0,1,7,2,4,5,6,88,9,3,1,2,3]"); 16 | 17 | return nodeX.Diff(nodeY, new JsonDiffOptions 18 | { 19 | SuppressDetectArrayMove = true, 20 | JsonElementComparison = JsonElementComparison.Semantic 21 | })!; 22 | } 23 | 24 | [Benchmark] 25 | public JToken Lcs_JsonNet() 26 | { 27 | var tokenX = JToken.Parse("[1,2,3,0,1,2,3,4,5,6,7,8,9,10,1,2,3]"); 28 | var tokenY = JToken.Parse("[1,2,3,10,0,1,7,2,4,5,6,88,9,3,1,2,3]"); 29 | 30 | return new JsonDiffPatchDotNet.JsonDiffPatch().Diff(tokenX, tokenY); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.Benchmark/JsonFileBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using BenchmarkDotNet.Attributes; 3 | 4 | namespace SystemTextJson.JsonDiffPatch.Benchmark 5 | { 6 | [IterationCount(50)] 7 | public abstract class JsonFileBenchmark 8 | { 9 | [ParamsAllValues] 10 | public virtual JsonFileSize FileSize { get; set; } 11 | 12 | protected string JsonLeft { get; set; } = null!; 13 | protected string JsonRight { get; set; } = null!; 14 | protected string JsonDiff { get; set; } = null!; 15 | 16 | [GlobalSetup] 17 | public virtual void Setup() 18 | { 19 | JsonLeft = File.ReadAllText(GetFilePath(FileSize, "left")); 20 | JsonRight = File.ReadAllText(GetFilePath(FileSize, "right")); 21 | JsonDiff = File.ReadAllText(GetFilePath(FileSize, "diff")); 22 | 23 | static string GetFilePath(JsonFileSize fileSize, string suffix) 24 | { 25 | var fileName = $"{fileSize:G}_{suffix}.json"; 26 | return $"Examples/{fileName.ToLowerInvariant()}"; 27 | } 28 | } 29 | 30 | public enum JsonFileSize 31 | { 32 | Small, 33 | Large 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.Benchmark/DeepEqualsJsonFileBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.JsonDiffPatch; 3 | using System.Text.Json.Nodes; 4 | using BenchmarkDotNet.Attributes; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace SystemTextJson.JsonDiffPatch.Benchmark 8 | { 9 | public class DeepEqualsJsonFileBenchmark : JsonFileBenchmark 10 | { 11 | [Benchmark] 12 | public bool SystemTextJson_Node() 13 | { 14 | var json1 = JsonNode.Parse(JsonLeft); 15 | var json2 = JsonNode.Parse(JsonLeft); 16 | 17 | return json1.DeepEquals(json2, JsonElementComparison.Semantic); 18 | } 19 | 20 | [Benchmark] 21 | public bool SystemTextJson_Document() 22 | { 23 | using var json1 = JsonDocument.Parse(JsonLeft); 24 | using var json2 = JsonDocument.Parse(JsonLeft); 25 | 26 | return json1.DeepEquals(json2, JsonElementComparison.Semantic); 27 | } 28 | 29 | [Benchmark] 30 | public bool JsonNet() 31 | { 32 | var json1 = JToken.Parse(JsonLeft); 33 | var json2 = JToken.Parse(JsonLeft); 34 | 35 | return JToken.DeepEquals(json1, json2); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.UnitTests/DefaultOptionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.JsonDiffPatch; 3 | using Xunit; 4 | 5 | namespace SystemTextJson.JsonDiffPatch.UnitTests 6 | { 7 | public class DefaultOptionsTests : IDisposable 8 | { 9 | private readonly JsonElementComparison _comparisonMode; 10 | 11 | public DefaultOptionsTests() 12 | { 13 | _comparisonMode = JsonDiffPatcher.DefaultComparison; 14 | } 15 | 16 | [Fact] 17 | public void DefaultDeepEqualsComparison_ComparerOptions() 18 | { 19 | JsonDiffPatcher.DefaultComparison = JsonElementComparison.Semantic; 20 | 21 | JsonComparerOptions comparerOptions = default; 22 | 23 | Assert.Equal(JsonDiffPatcher.DefaultComparison, comparerOptions.JsonElementComparison); 24 | } 25 | 26 | [Fact] 27 | public void DefaultDeepEqualsComparison_DiffOptions() 28 | { 29 | JsonDiffPatcher.DefaultComparison = JsonElementComparison.Semantic; 30 | 31 | var diffOptions = new JsonDiffOptions(); 32 | 33 | Assert.Equal(JsonDiffPatcher.DefaultComparison, diffOptions.JsonElementComparison); 34 | } 35 | 36 | public void Dispose() 37 | { 38 | JsonDiffPatcher.DefaultComparison = _comparisonMode; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.Benchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Columns; 2 | using BenchmarkDotNet.Configs; 3 | using BenchmarkDotNet.Diagnosers; 4 | using BenchmarkDotNet.Exporters; 5 | using BenchmarkDotNet.Loggers; 6 | using BenchmarkDotNet.Running; 7 | using BenchmarkDotNet.Validators; 8 | 9 | namespace SystemTextJson.JsonDiffPatch.Benchmark 10 | { 11 | class Program 12 | { 13 | static void Main(string[] args) 14 | { 15 | var config = new ManualConfig(); 16 | config.AddColumn( 17 | TargetMethodColumn.Method, 18 | StatisticColumn.Mean, 19 | StatisticColumn.Median, 20 | StatisticColumn.P80, 21 | StatisticColumn.P95, 22 | StatisticColumn.Min, 23 | StatisticColumn.Max); 24 | config.AddColumnProvider(DefaultColumnProviders.Params); 25 | config.AddColumnProvider(DefaultColumnProviders.Metrics); 26 | config.AddDiagnoser(new MemoryDiagnoser(new MemoryDiagnoserConfig(false))); 27 | config.AddValidator(JitOptimizationsValidator.FailOnError); 28 | config.AddLogger(new ConsoleLogger(true)); 29 | config.AddExporter(MarkdownExporter.GitHub); 30 | 31 | BenchmarkSwitcher.FromAssembly(typeof(BenchmarkHelper).Assembly).Run(args, config); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net7.0;net6.0;netstandard2.0;net462 5 | enable 6 | latest 7 | true 8 | $(NoWarn);CS1591 9 | 10 | 11 | 12 | 2.0.0 13 | Wei Chen 14 | https://github.com/weichch/system-text-json-jsondiffpatch 15 | Copyright © Wei Chen 2024 16 | icon.png 17 | LICENSE 18 | https://github.com/weichch/system-text-json-jsondiffpatch/blob/$(JsonDiffPatchPackageVersion)/ReleaseNotes.md 19 | 20 | 21 | 22 | $(JsonDiffPatchPackageVersion) 23 | 1.0.0.0 24 | 1.0.0.0 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Diffs/JsonDiffContext.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | 3 | namespace System.Text.Json.JsonDiffPatch.Diffs 4 | { 5 | /// 6 | /// Represents JSON diff context. 7 | /// 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 | /// 20 | /// Gets the left value in comparison. 21 | /// 22 | /// The type of left value. 23 | public T Left() 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 | /// 34 | /// Gets the right value in comparison. 35 | /// 36 | /// The type of right value. 37 | public T Right() 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 | } 48 | -------------------------------------------------------------------------------- /test/Examples/demo_diff.json: -------------------------------------------------------------------------------- 1 | { 2 | "summary": [ 3 | "@@ -638,17 +638,17 @@\n via, Bra\n-z\n+s\n il, %0aCh\n@@ -916,20 +916,13 @@\n re a\n-lso known as\n+.k.a.\n Car\n", 4 | 0, 5 | 2 6 | ], 7 | "surface": [ 8 | 17840000, 9 | 0, 10 | 0 11 | ], 12 | "demographics": { 13 | "population": [ 14 | 385742554, 15 | 385744896 16 | ] 17 | }, 18 | "languages": { 19 | "2": [ 20 | "inglés" 21 | ], 22 | "_t": "a", 23 | "_2": [ 24 | "english", 25 | 0, 26 | 0 27 | ] 28 | }, 29 | "countries": { 30 | "0": { 31 | "capital": [ 32 | "Buenos Aires", 33 | "Rawson" 34 | ] 35 | }, 36 | "9": [ 37 | { 38 | "name": "Antártida", 39 | "unasur": false 40 | } 41 | ], 42 | "10": { 43 | "population": [ 44 | 42888594 45 | ] 46 | }, 47 | "_t": "a", 48 | "_4": [ 49 | "", 50 | 10, 51 | 3 52 | ], 53 | "_8": [ 54 | "", 55 | 2, 56 | 3 57 | ], 58 | "_10": [ 59 | { 60 | "name": "Uruguay", 61 | "capital": "Montevideo", 62 | "independence": "1825-08-24T12:20:56.000Z", 63 | "unasur": true 64 | }, 65 | 0, 66 | 0 67 | ], 68 | "_11": [ 69 | { 70 | "name": "Venezuela", 71 | "capital": "Caracas", 72 | "independence": "1811-07-04T12:20:56.000Z", 73 | "unasur": true 74 | }, 75 | 0, 76 | 0 77 | ] 78 | }, 79 | "spanishName": [ 80 | "Sudamérica" 81 | ] 82 | } -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.Benchmark/BenchmarkHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch; 2 | using System.Text.Json.JsonDiffPatch.Diffs; 3 | using System.Text.Json.Nodes; 4 | using JsonDiffPatchDotNet; 5 | 6 | namespace SystemTextJson.JsonDiffPatch.Benchmark 7 | { 8 | internal static class BenchmarkHelper 9 | { 10 | public static JsonDiffPatchDotNet.JsonDiffPatch CreateJsonNetDiffPatch() 11 | { 12 | return new JsonDiffPatchDotNet.JsonDiffPatch(new Options 13 | { 14 | TextDiff = TextDiffMode.Simple 15 | }); 16 | } 17 | 18 | public static JsonDiffOptions CreateDiffOptionsWithJsonNetMatch() 19 | { 20 | return new JsonDiffOptions 21 | { 22 | TextDiffMinLength = 0, 23 | SuppressDetectArrayMove = true, 24 | ArrayItemMatcher = JsonNetArrayItemMatch, 25 | JsonElementComparison = JsonElementComparison.Semantic 26 | }; 27 | } 28 | 29 | // Simulate array item match algorithm in JsonNet version: 30 | // https://github.com/wbish/jsondiffpatch.net/blob/master/Src/JsonDiffPatchDotNet/Lcs.cs#L51 31 | private static bool JsonNetArrayItemMatch(ref ArrayItemMatchContext context) 32 | { 33 | if (context.Left is JsonObject && context.Right is JsonObject || 34 | context.Left is JsonArray && context.Right is JsonArray) 35 | { 36 | return true; 37 | } 38 | 39 | return false; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.Benchmark/QuickDeepEquals.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.JsonDiffPatch; 3 | using System.Text.Json.Nodes; 4 | using BenchmarkDotNet.Attributes; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace SystemTextJson.JsonDiffPatch.Benchmark 8 | { 9 | [IterationCount(10)] 10 | public class QuickDeepEquals : JsonFileBenchmark 11 | { 12 | [Params(JsonFileSize.Small)] 13 | public override JsonFileSize FileSize { get; set; } 14 | 15 | [Benchmark] 16 | public bool SystemTextJson() 17 | { 18 | var node1 = JsonNode.Parse(JsonLeft); 19 | var node2 = JsonNode.Parse(JsonLeft); 20 | 21 | return node1.DeepEquals(node2, JsonElementComparison.Semantic); 22 | } 23 | 24 | [Benchmark] 25 | public bool SystemTextJson_Document() 26 | { 27 | var node1 = JsonDocument.Parse(JsonLeft); 28 | var node2 = JsonDocument.Parse(JsonLeft); 29 | 30 | return node1.DeepEquals(node2, JsonElementComparison.Semantic); 31 | } 32 | 33 | [Benchmark] 34 | public bool SystemTextJson_Document_RawText() 35 | { 36 | var node1 = JsonDocument.Parse(JsonLeft); 37 | var node2 = JsonDocument.Parse(JsonLeft); 38 | 39 | return node1.DeepEquals(node2); 40 | } 41 | 42 | [Benchmark] 43 | public JToken JsonNet() 44 | { 45 | var token1 = JToken.Parse(JsonLeft); 46 | var token2 = JToken.Parse(JsonLeft); 47 | 48 | return JToken.DeepEquals(token1, token2); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch.NUnit/JsonDiffConstraint.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | using NUnit.Framework.Constraints; 3 | 4 | namespace System.Text.Json.JsonDiffPatch.Nunit 5 | { 6 | /// 7 | /// Represents a constraint that tests whether two JSON. 8 | /// 9 | public abstract class JsonDiffConstraint : Constraint 10 | { 11 | private readonly JsonNode? _expected; 12 | private JsonDiffOptions? _diffOptions; 13 | private Func? _outputFormatter; 14 | 15 | protected JsonDiffConstraint(JsonNode? expected) 16 | { 17 | _expected = expected; 18 | } 19 | 20 | public virtual Func? OutputFormatter => _outputFormatter; 21 | public JsonNode? Delta { get; private set; } 22 | 23 | public JsonDiffConstraint WithDiffOptions(JsonDiffOptions diffOptions) 24 | { 25 | _diffOptions = diffOptions ?? throw new ArgumentNullException(nameof(diffOptions)); 26 | return this; 27 | } 28 | 29 | public JsonDiffConstraint WithOutputFormatter(Func outputFormatter) 30 | { 31 | _outputFormatter = outputFormatter ?? throw new ArgumentNullException(nameof(outputFormatter)); 32 | return this; 33 | } 34 | 35 | public override ConstraintResult ApplyTo(TActual actual) 36 | { 37 | Delta = _expected.Diff((JsonNode?) (object?) actual, _diffOptions); 38 | return new JsonDiffConstraintResult(this, actual, Test()); 39 | } 40 | 41 | protected abstract bool Test(); 42 | } 43 | } -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.Benchmark/DiffJsonFileBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.JsonDiffPatch; 3 | using System.Text.Json.JsonDiffPatch.Diffs.Formatters; 4 | using System.Text.Json.Nodes; 5 | using BenchmarkDotNet.Attributes; 6 | using JsonDiffPatchDotNet.Formatters.JsonPatch; 7 | using Newtonsoft.Json.Linq; 8 | 9 | namespace SystemTextJson.JsonDiffPatch.Benchmark 10 | { 11 | public class DiffJsonFileBenchmark : JsonFileBenchmark 12 | { 13 | [Benchmark] 14 | public JsonNode? SystemTextJson() 15 | { 16 | var node1 = JsonNode.Parse(JsonLeft); 17 | var node2 = JsonNode.Parse(JsonRight); 18 | 19 | return node1.Diff(node2, BenchmarkHelper.CreateDiffOptionsWithJsonNetMatch()); 20 | } 21 | 22 | [Benchmark] 23 | public JToken JsonNet() 24 | { 25 | var token1 = JToken.Parse(JsonLeft); 26 | var token2 = JToken.Parse(JsonRight); 27 | 28 | return BenchmarkHelper.CreateJsonNetDiffPatch().Diff(token1, token2); 29 | } 30 | 31 | [Benchmark] 32 | public JsonNode? SystemTextJson_Rfc() 33 | { 34 | var node1 = JsonNode.Parse(JsonLeft); 35 | var node2 = JsonNode.Parse(JsonRight); 36 | 37 | return node1.Diff(node2, new JsonPatchDeltaFormatter(), 38 | BenchmarkHelper.CreateDiffOptionsWithJsonNetMatch()); 39 | } 40 | 41 | [Benchmark] 42 | public IList JsonNet_Rfc() 43 | { 44 | var token1 = JToken.Parse(JsonLeft); 45 | var token2 = JToken.Parse(JsonRight); 46 | 47 | return new JsonDeltaFormatter().Format( 48 | BenchmarkHelper.CreateJsonNetDiffPatch().Diff(token1, token2)); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Diffs/ArrayItemMatchContext.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | 3 | namespace System.Text.Json.JsonDiffPatch.Diffs 4 | { 5 | /// 6 | /// Context for matching array items. 7 | /// 8 | public struct ArrayItemMatchContext 9 | { 10 | /// 11 | /// Creates an instance of the context. 12 | /// 13 | /// The left item. 14 | /// The index of the left item. 15 | /// The right item. 16 | /// The index of the right item. 17 | public ArrayItemMatchContext(JsonNode? left, int leftPos, JsonNode? right, int rightPos) 18 | { 19 | Left = left; 20 | LeftPosition = leftPos; 21 | Right = right; 22 | RightPosition = rightPos; 23 | IsDeepEqual = false; 24 | } 25 | 26 | /// 27 | /// Gets the left item. 28 | /// 29 | public JsonNode? Left { get; } 30 | 31 | /// 32 | /// Gets the index of the left item. 33 | /// 34 | public int LeftPosition { get; } 35 | 36 | /// 37 | /// Gets the right item. 38 | /// 39 | public JsonNode? Right { get; } 40 | 41 | /// 42 | /// Gets the index of the right item. 43 | /// 44 | public int RightPosition { get; } 45 | 46 | /// 47 | /// Gets whether the result was the two items are deeply equal. 48 | /// 49 | public bool IsDeepEqual { get; private set; } 50 | 51 | /// 52 | /// Sets to true. 53 | /// 54 | public void DeepEqual() 55 | { 56 | IsDeepEqual = true; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.Benchmark/SystemTextJson.JsonDiffPatch.Benchmark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | PreserveNewest 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | PreserveNewest 30 | 31 | 32 | PreserveNewest 33 | 34 | 35 | PreserveNewest 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ReleaseNotes.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## 2.0.0 4 | 5 | - This version contains several **BREAKING CHANGES**: 6 | - **Targeting framework changes**: 7 | - Added: .NET 8, .NET 7, .NET 6, .NET Framework 4.6.2 8 | - Removed: .NET Standard 2.1, .NET Framework 4.6.1 9 | - Minimum version of `System.Text.Json` required is bumped up to `8.0.0` 10 | - `JsonDiffPatcher.DeepEquals(JsonNode)` now simply calls `JsonNode.DeepEquals(JsonNode, JsonNode)` method introduced in [this issue](https://github.com/dotnet/runtime/issues/56592) 11 | - `JsonDiffPatcher.Diff` method is unchanged because it does not use `JsonNode.DeepEquals(JsonNode, JsonNode)` method internally 12 | - You can still use `JsonDiffPatcher.DeepEquals` method when invoked with custom comparison options 13 | - When invoked against `JsonDocument` and `JsonElement`, `DeepEquals` method is unchanged 14 | - Removed `JsonDiffPatcher.DeepClone` method. You can migrate to `JsonNode.DeepClone` method introduced in [this issue](https://github.com/dotnet/runtime/issues/56592) 15 | 16 | ## 1.3.1 17 | 18 | - Added `PropertyFilter` to `JsonDiffOptions` (#29) 19 | - Fixed bug in diffing null-valued properties (#31) 20 | 21 | ## 1.3.0 22 | 23 | - **Added `DeepEquals` implementation for `JsonDocument` and `JsonElement`** 24 | - Performance improvements in raw text comparison mode 25 | - Removed unnecessary allocation when default diff option is used 26 | - Removed one `DeepEquals` overload that was accidentally exposed as a public method 27 | 28 | ## 1.2.0 29 | 30 | - Major performance improvement in array comparison 31 | - Performance improvements in `DeepEquals` and `JsonValueComparer` 32 | - **[BREAKING CHANGE]** Breaking changes in array comparison: 33 | - Removed `JsonDiffOptions.PreferFuzzyArrayItemMatch` option 34 | - `JsonDiffOptions.ArrayItemMatcher` is only invoked when array items are not deeply equal 35 | - **[BREAKING CHANGE]** Base64 encoded text is considered as long text if length is greater than or equal to `JsonDiffOptions.TextDiffMinLength` 36 | 37 | ## 1.1.0 38 | 39 | - Added `JsonValueComparer` that implements semantic comparison of two `JsonValue` objects (including the ones backed by `JsonElement`) 40 | - **[BREAKING CHANGE]** `Diff` method no longer uses `object.Equals` to compare values encapsulated in `JsonValue`. `JsonValueComparer` is used instead 41 | - Added semantic equality to `DeepEquals` method 42 | - Added options to `JsonDiffOptions` to enable semantic diff 43 | - Added `JsonDiffPatcher.DefaultOptions` for customizing default diff options 44 | 45 | ## 1.0.0 46 | 47 | - Initial release -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Patching/JsonDiffPatcher.Text.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Text.Json.JsonDiffPatch.Diffs; 3 | using System.Text.Json.Nodes; 4 | using DiffMatchPatch; 5 | 6 | namespace System.Text.Json.JsonDiffPatch 7 | { 8 | static partial class JsonDiffPatcher 9 | { 10 | private static JsonValue PatchLongText(string text, JsonDiffDelta delta, JsonPatchOptions options) 11 | { 12 | // When make changes in this method, also copy the changes to ReversePatch* method 13 | 14 | var textPatch = options.TextPatchProvider ?? DefaultLongTextPatch; 15 | return JsonValue.Create(textPatch(text, delta.GetTextDiff()))!; 16 | 17 | static string DefaultLongTextPatch(string text, string patchText) 18 | { 19 | var alg = DefaultTextDiffAlgorithm.Value; 20 | var patches = alg.patch_fromText(patchText); 21 | var results = alg.patch_apply(patches, text); 22 | 23 | if (((bool[]) results[1]).Any(flag => !flag)) 24 | { 25 | throw new InvalidOperationException(TextPatchFailed); 26 | } 27 | 28 | return (string) results[0]; 29 | } 30 | } 31 | 32 | private static JsonValue ReversePatchLongText(string text, JsonDiffDelta delta, JsonReversePatchOptions options) 33 | { 34 | // When make changes in this method, also copy the changes to Patch* method 35 | 36 | var textPatch = options.ReverseTextPatchProvider ?? DefaultLongTextReversePatch; 37 | return JsonValue.Create(textPatch(text, delta.GetTextDiff()))!; 38 | 39 | static string DefaultLongTextReversePatch(string text, string patchText) 40 | { 41 | var alg = DefaultTextDiffAlgorithm.Value; 42 | var patches = alg.patch_fromText(patchText); 43 | 44 | // Reverse patches 45 | var reversedPatches = alg.patch_deepCopy(patches); 46 | foreach (var diff in reversedPatches.SelectMany(p => p.diffs)) 47 | { 48 | diff.operation = diff.operation switch 49 | { 50 | Operation.DELETE => Operation.INSERT, 51 | Operation.INSERT => Operation.DELETE, 52 | _ => diff.operation 53 | }; 54 | } 55 | 56 | var results = alg.patch_apply(reversedPatches, text); 57 | 58 | if (((bool[])results[1]).Any(flag => !flag)) 59 | { 60 | throw new InvalidOperationException(TextPatchFailed); 61 | } 62 | 63 | return (string)results[0]; 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Diffs/JsonDiffPatcher.Object.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.JsonDiffPatch.Diffs; 3 | using System.Text.Json.Nodes; 4 | 5 | namespace System.Text.Json.JsonDiffPatch 6 | { 7 | static partial class JsonDiffPatcher 8 | { 9 | // Object diff: 10 | // https://github.com/benjamine/jsondiffpatch/blob/master/docs/deltas.md#object-with-inner-changes 11 | private static void DiffObject( 12 | ref JsonDiffDelta delta, 13 | JsonObject left, 14 | JsonObject right, 15 | JsonDiffOptions? options) 16 | { 17 | var leftProperties = (left as IDictionary).Keys; 18 | var rightProperties = (right as IDictionary).Keys; 19 | 20 | JsonDiffContext? diffContext = null; 21 | var propertyFilter = options?.PropertyFilter; 22 | if (propertyFilter is not null) 23 | { 24 | diffContext = new JsonDiffContext(left, right); 25 | } 26 | 27 | foreach (var prop in leftProperties) 28 | { 29 | if (propertyFilter is not null && !propertyFilter(prop, diffContext!)) 30 | { 31 | continue; 32 | } 33 | 34 | var leftValue = left[prop]; 35 | if (!right.TryGetPropertyValue(prop, out var rightValue)) 36 | { 37 | // Deleted: https://github.com/benjamine/jsondiffpatch/blob/master/docs/deltas.md#deleted 38 | delta.ObjectChange(prop, JsonDiffDelta.CreateDeleted(leftValue)); 39 | } 40 | else 41 | { 42 | // Modified: https://github.com/benjamine/jsondiffpatch/blob/master/docs/deltas.md#modified 43 | var valueDiff = new JsonDiffDelta(); 44 | DiffInternal(ref valueDiff, leftValue, rightValue, options); 45 | if (valueDiff.Document is not null) 46 | { 47 | delta.ObjectChange(prop, valueDiff); 48 | } 49 | } 50 | } 51 | 52 | foreach (var prop in rightProperties) 53 | { 54 | if (propertyFilter is not null && !propertyFilter(prop, diffContext!)) 55 | { 56 | continue; 57 | } 58 | 59 | var rightValue = right[prop]; 60 | if (!left.ContainsKey(prop)) 61 | { 62 | // Added: https://github.com/benjamine/jsondiffpatch/blob/master/docs/deltas.md#added 63 | delta.ObjectChange(prop, JsonDiffDelta.CreateAdded(rightValue)); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.UnitTests/SystemTextJson.JsonDiffPatch.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | runtime; build; native; contentfiles; analyzers; buildtransitive 8 | all 9 | 10 | 11 | runtime; build; native; contentfiles; analyzers; buildtransitive 12 | all 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | PreserveNewest 23 | 24 | 25 | Examples\demo_diff_jsonpatch.json 26 | PreserveNewest 27 | 28 | 29 | PreserveNewest 30 | 31 | 32 | Examples\demo_diff.json 33 | PreserveNewest 34 | 35 | 36 | PreserveNewest 37 | 38 | 39 | Examples\large_diff.json 40 | PreserveNewest 41 | 42 | 43 | PreserveNewest 44 | 45 | 46 | 47 | 48 | 49 | Examples\demo_diff_notext.json 50 | PreserveNewest 51 | 52 | 53 | Examples\large_diff_notext.json 54 | PreserveNewest 55 | 56 | 57 | Examples\large_diff_jsonpatch.json 58 | PreserveNewest 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Diffs/JsonDiffOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Runtime.CompilerServices; 3 | using System.Text.Json.JsonDiffPatch.Diffs; 4 | using System.Text.Json.Nodes; 5 | 6 | namespace System.Text.Json.JsonDiffPatch 7 | { 8 | /// 9 | /// Represents options for making JSON diff. 10 | /// 11 | public class JsonDiffOptions 12 | { 13 | private JsonElementComparison? _jsonElementComparison; 14 | 15 | /// 16 | /// Specifies whether to suppress detect array move. Default value is false. 17 | /// 18 | public bool SuppressDetectArrayMove { get; set; } 19 | 20 | /// 21 | /// Gets or sets custom function to match array items when array items are found not equal using default match 22 | /// algorithm. 23 | /// 24 | public ArrayItemMatch? ArrayItemMatcher { get; set; } 25 | 26 | /// 27 | /// Gets or sets the function to find key of a or . 28 | /// 29 | public Func? ArrayObjectItemKeyFinder { get; set; } 30 | 31 | /// 32 | /// Gets or sets whether two JSON objects (object or array) should be considered equal if they are at the same 33 | /// index inside their parent arrays. 34 | /// 35 | public bool ArrayObjectItemMatchByPosition { get; set; } 36 | 37 | /// 38 | /// Gets or sets the minimum length of texts that should be compared using long text comparison algorithm, e.g. 39 | /// diff_match_patch from Google. If this property is set to 0, long text comparison algorithm is 40 | /// disabled. Default value is 0. 41 | /// 42 | public int TextDiffMinLength { get; set; } 43 | 44 | /// 45 | /// Gets or sets the function to diff long texts. 46 | /// 47 | public Func? TextDiffProvider { get; set; } 48 | 49 | /// 50 | /// Gets or sets the mode to compare two instances. 51 | /// 52 | public JsonElementComparison JsonElementComparison 53 | { 54 | get => _jsonElementComparison ?? JsonDiffPatcher.DefaultComparison; 55 | set => _jsonElementComparison = value; 56 | } 57 | 58 | /// 59 | /// Gets or sets the comparer. 60 | /// 61 | public IEqualityComparer? ValueComparer { get; set; } 62 | 63 | /// 64 | /// Gets or sets the filter function to ignore JSON property. To ignore a property, 65 | /// implement this function and return false. 66 | /// 67 | public Func? PropertyFilter { get; set; } 68 | 69 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 70 | internal JsonComparerOptions CreateComparerOptions() => new(JsonElementComparison, ValueComparer); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Patching/JsonDiffPatcher.Array.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch.Diffs; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace System.Text.Json.JsonDiffPatch 5 | { 6 | static partial class JsonDiffPatcher 7 | { 8 | private static void PatchArray(JsonArray left, JsonObject patch, JsonPatchOptions options) 9 | { 10 | // When make changes in this method, also copy the changes to ReversePatch* method 11 | 12 | var arrayDelta = new JsonDiffDelta(patch); 13 | foreach (var entry in arrayDelta.GetPatchableArrayChangeEnumerable(left)) 14 | { 15 | var delta = entry.Diff; 16 | var kind = delta.Kind; 17 | if (kind == DeltaKind.Deleted) 18 | { 19 | CheckForIndex(entry.Index, left.Count - 1); 20 | left.RemoveAt(entry.Index); 21 | } 22 | else if (kind == DeltaKind.Added) 23 | { 24 | CheckForIndex(entry.Index, left.Count); 25 | left.Insert(entry.Index, delta.GetAdded()); 26 | } 27 | else 28 | { 29 | CheckForIndex(entry.Index, left.Count - 1); 30 | var value = left[entry.Index]; 31 | var oldValue = value; 32 | Patch(ref value, delta.Document, options); 33 | if (!ReferenceEquals(oldValue, value)) 34 | { 35 | left[entry.Index] = value; 36 | } 37 | } 38 | } 39 | } 40 | 41 | private static void ReversePatchArray(JsonArray left, JsonObject patch, JsonReversePatchOptions options) 42 | { 43 | // When make changes in this method, also copy the changes to Patch* method 44 | 45 | var arrayDelta = new JsonDiffDelta(patch); 46 | foreach (var entry in arrayDelta.GetPatchableArrayChangeEnumerable(left, true)) 47 | { 48 | var delta = entry.Diff; 49 | var kind = delta.Kind; 50 | if (kind == DeltaKind.Deleted) 51 | { 52 | CheckForIndex(entry.Index, left.Count); 53 | left.Insert(entry.Index, delta.GetDeleted()); 54 | } 55 | else if (kind == DeltaKind.Added) 56 | { 57 | CheckForIndex(entry.Index, left.Count - 1); 58 | left.RemoveAt(entry.Index); 59 | } 60 | else 61 | { 62 | CheckForIndex(entry.Index, left.Count - 1); 63 | var value = left[entry.Index]; 64 | var oldValue = value; 65 | ReversePatch(ref value, delta.Document, options); 66 | if (!ReferenceEquals(oldValue, value)) 67 | { 68 | left[entry.Index] = value; 69 | } 70 | } 71 | } 72 | } 73 | 74 | private static void CheckForIndex(int index, int upperLimit) 75 | { 76 | if (index > upperLimit) 77 | { 78 | throw new FormatException(JsonDiffDelta.InvalidPatchDocument); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/Examples/demo_diff_jsonpatch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "op": "replace", 4 | "path": "/summary", 5 | "value": "South America (Spanish: Am\u00E9rica del Sur, Sudam\u00E9rica or \nSuram\u00E9rica; Portuguese: Am\u00E9rica do Sul; Quechua and Aymara: \nUrin Awya Yala; Guarani: \u00D1embyam\u00E9rika; Dutch: Zuid-Amerika; \nFrench: Am\u00E9rique du Sud) is a continent situated in the \nWestern Hemisphere, mostly in the Southern Hemisphere, with \na relatively small portion in the Northern Hemisphere. \nThe continent is also considered a subcontinent of the \nAmericas.[2][3] It is bordered on the west by the Pacific \nOcean and on the north and east by the Atlantic Ocean; \nNorth America and the Caribbean Sea lie to the northwest. \nIt includes twelve countries: Argentina, Bolivia, Brasil, \nChile, Colombia, Ecuador, Guyana, Paraguay, Peru, Suriname, \nUruguay, and Venezuela. The South American nations that \nborder the Caribbean Sea\u2014including Colombia, Venezuela, \nGuyana, Suriname, as well as French Guiana, which is an \noverseas region of France\u2014are a.k.a. Caribbean South \nAmerica. South America has an area of 17,840,000 square \nkilometers (6,890,000 sq mi). Its population as of 2005 \nhas been estimated at more than 371,090,000. South America \nranks fourth in area (after Asia, Africa, and North America) \nand fifth in population (after Asia, Africa, Europe, and \nNorth America). The word America was coined in 1507 by \ncartographers Martin Waldseem\u00FCller and Matthias Ringmann, \nafter Amerigo Vespucci, who was the first European to \nsuggest that the lands newly discovered by Europeans were \nnot India, but a New World unknown to Europeans." 6 | }, 7 | { 8 | "op": "remove", 9 | "path": "/surface" 10 | }, 11 | { 12 | "op": "replace", 13 | "path": "/demographics/population", 14 | "value": 385744896 15 | }, 16 | { 17 | "op": "remove", 18 | "path": "/languages/2" 19 | }, 20 | { 21 | "op": "add", 22 | "path": "/languages/2", 23 | "value": "ingl\u00E9s" 24 | }, 25 | { 26 | "op": "remove", 27 | "path": "/countries/11" 28 | }, 29 | { 30 | "op": "remove", 31 | "path": "/countries/10" 32 | }, 33 | { 34 | "op": "remove", 35 | "path": "/countries/8" 36 | }, 37 | { 38 | "op": "remove", 39 | "path": "/countries/4" 40 | }, 41 | { 42 | "op": "remove", 43 | "path": "/countries/0" 44 | }, 45 | { 46 | "op": "add", 47 | "path": "/countries/0", 48 | "value": { 49 | "name": "Argentina", 50 | "capital": "Rawson", 51 | "independence": "1816-07-08T12:20:56.000Z", 52 | "unasur": true 53 | } 54 | }, 55 | { 56 | "op": "add", 57 | "path": "/countries/2", 58 | "value": { 59 | "name": "Peru", 60 | "capital": "Lima", 61 | "independence": "1821-07-27T12:20:56.000Z", 62 | "unasur": true 63 | } 64 | }, 65 | { 66 | "op": "add", 67 | "path": "/countries/9", 68 | "value": { 69 | "name": "Ant\u00E1rtida", 70 | "unasur": false 71 | } 72 | }, 73 | { 74 | "op": "add", 75 | "path": "/countries/10", 76 | "value": { 77 | "name": "Colombia", 78 | "capital": "Bogot\u00E1", 79 | "independence": "1810-07-19T12:20:56.000Z", 80 | "unasur": true, 81 | "population": 42888594 82 | } 83 | }, 84 | { 85 | "op": "add", 86 | "path": "/spanishName", 87 | "value": "Sudam\u00E9rica" 88 | } 89 | ] -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.UnitTests/NodeTests/JsonValueComparerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch; 2 | using System.Text.Json.Nodes; 3 | using Xunit; 4 | 5 | namespace SystemTextJson.JsonDiffPatch.UnitTests.NodeTests 6 | { 7 | public class JsonValueComparerTests 8 | { 9 | [Theory] 10 | [InlineData("1", "2")] 11 | [InlineData("1.0", "10")] 12 | [InlineData("\"2019-11-27T09:00:00.000\"", "\"2019-11-27T10:00:00.000\"")] 13 | [InlineData("\"819f0a05-905d-4c40-a3d7-2e033757496b\"", "\"819f0a05-905d-4c40-a3d7-2e033757496c\"")] 14 | [InlineData("\"Shaun is a rabbit\"", "\"Shawn is a rabbit\"")] 15 | [InlineData("\"2019-11-27T09:00:00.000\"", "\"Shaun is a rabbit\"")] 16 | [InlineData("1", "\"Shaun is a rabbit\"")] 17 | [InlineData("true", "\"Shaun is a rabbit\"")] 18 | [InlineData("true", "1")] 19 | [InlineData("false", "true")] 20 | [InlineData(null, "1")] 21 | [InlineData("null", "1")] 22 | public void Compare_LessThanZero(string? json1, string? json2) 23 | { 24 | var node1 = json1 == null ? null : JsonNode.Parse(json1)?.AsValue(); 25 | var node2 = json2 == null ? null : JsonNode.Parse(json2)?.AsValue(); 26 | 27 | var result = JsonValueComparer.Compare(node1, node2); 28 | 29 | Assert.True(result < 0); 30 | } 31 | 32 | [Theory] 33 | [InlineData("2", "1")] 34 | [InlineData("10", "1.0")] 35 | [InlineData("\"2019-11-27T10:00:00.000\"", "\"2019-11-27T09:00:00.000\"")] 36 | [InlineData("\"819f0a05-905d-4c40-a3d7-2e033757496c\"", "\"819f0a05-905d-4c40-a3d7-2e033757496b\"")] 37 | [InlineData("\"Shawn is a rabbit\"", "\"Shaun is a rabbit\"")] 38 | [InlineData("\"Shaun is a rabbit\"", "\"2019-11-27T09:00:00.000\"")] 39 | [InlineData("\"Shaun is a rabbit\"", "1")] 40 | [InlineData("\"Shaun is a rabbit\"", "true")] 41 | [InlineData("1", "true")] 42 | [InlineData("true", "false")] 43 | [InlineData("1", null)] 44 | [InlineData("1", "null")] 45 | public void Compare_GreaterThanZero(string? json1, string? json2) 46 | { 47 | var node1 = json1 == null ? null : JsonNode.Parse(json1)?.AsValue(); 48 | var node2 = json2 == null ? null : JsonNode.Parse(json2)?.AsValue(); 49 | 50 | var result = JsonValueComparer.Compare(node1, node2); 51 | 52 | Assert.True(result > 0); 53 | } 54 | 55 | [Theory] 56 | [InlineData("1", "1")] 57 | [InlineData("1.0", "1")] 58 | [InlineData("10", "1.0e1")] 59 | [InlineData("\"2019-11-27T00:00:00.000\"", "\"2019-11-27\"")] 60 | [InlineData("\"819f0a05-905d-4c40-a3d7-2e033757496b\"", "\"819F0A05-905D-4C40-A3D7-2E033757496B\"")] 61 | [InlineData("\"Shaun is a rabbit\"", "\"Shaun is a rabbit\"")] 62 | [InlineData("true", "true")] 63 | [InlineData("false", "false")] 64 | [InlineData("null", null)] 65 | [InlineData(null, null)] 66 | [InlineData("null", "null")] 67 | public void Compare_EqualToZero(string? json1, string? json2) 68 | { 69 | var node1 = json1 == null ? null : JsonNode.Parse(json1)?.AsValue(); 70 | var node2 = json2 == null ? null : JsonNode.Parse(json2)?.AsValue(); 71 | 72 | var result = JsonValueComparer.Compare(node1, node2); 73 | 74 | Assert.Equal(0, result); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/JsonBytes.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace System.Text.Json.JsonDiffPatch 5 | { 6 | internal class JsonBytes : IBufferWriter, IDisposable 7 | { 8 | private const int DefaultBufferSize = 256; 9 | private byte[]? _buffer; 10 | private int _bufferHead; 11 | private bool _isDisposed; 12 | private readonly bool _writable; 13 | 14 | private static readonly JsonBytes Empty = new(false); 15 | 16 | private JsonBytes(bool writable) 17 | { 18 | _writable = writable; 19 | } 20 | 21 | public Utf8JsonReader GetReader() => _buffer is null 22 | ? default 23 | : new Utf8JsonReader(_buffer.AsSpan(0, _bufferHead)); 24 | 25 | void IBufferWriter.Advance(int count) 26 | { 27 | _bufferHead += count; 28 | } 29 | 30 | Memory IBufferWriter.GetMemory(int sizeHint) 31 | { 32 | EnsureBufferSize(sizeHint); 33 | return _buffer!.AsMemory(_bufferHead); 34 | } 35 | 36 | Span IBufferWriter.GetSpan(int sizeHint) 37 | { 38 | EnsureBufferSize(sizeHint); 39 | return _buffer!.AsSpan(sizeHint); 40 | } 41 | 42 | private void EnsureBufferSize(int sizeHint) 43 | { 44 | if (!_writable) 45 | { 46 | throw new InvalidOperationException("Buffer is not writable."); 47 | } 48 | 49 | if (_isDisposed) 50 | { 51 | throw new ObjectDisposedException("this"); 52 | } 53 | 54 | _buffer ??= ArrayPool.Shared.Rent(Math.Max(sizeHint, DefaultBufferSize)); 55 | 56 | if (_bufferHead + sizeHint <= _buffer.Length) 57 | { 58 | // We have enough space in buffer 59 | return; 60 | } 61 | 62 | // Resize buffer 63 | var addition = sizeHint - (_buffer.Length - _bufferHead); 64 | var oldBuffer = _buffer; 65 | // Rent a new buffer 66 | // If size of bytes requires is less than the default buffer size, use the default buffer size 67 | // to reduce future rent 68 | _buffer = ArrayPool.Shared.Rent(_buffer.Length + Math.Max(addition, DefaultBufferSize)); 69 | // Copy the old buffer 70 | oldBuffer.AsSpan(0, _bufferHead).CopyTo(_buffer.AsSpan()); 71 | oldBuffer.AsSpan(0, _bufferHead).Clear(); 72 | ArrayPool.Shared.Return(oldBuffer); 73 | } 74 | 75 | public void Dispose() 76 | { 77 | if (_isDisposed) 78 | { 79 | return; 80 | } 81 | 82 | _isDisposed = true; 83 | 84 | if (_buffer is null) 85 | { 86 | return; 87 | } 88 | 89 | _buffer.AsSpan(0, _bufferHead).Clear(); 90 | ArrayPool.Shared.Return(_buffer); 91 | _buffer = null; 92 | _bufferHead = 0; 93 | } 94 | 95 | public static JsonBytes FromNode(JsonNode? node, JsonSerializerOptions? serializerOptions) 96 | { 97 | if (node is null) 98 | { 99 | return Empty; 100 | } 101 | 102 | var jsonBytes = new JsonBytes(true); 103 | using var writer = new Utf8JsonWriter(jsonBytes); 104 | node.WriteTo(writer, serializerOptions); 105 | writer.Flush(); 106 | return jsonBytes; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /test/Examples/demo_right.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "South America", 3 | "summary": "South America (Spanish: América del Sur, Sudamérica or \nSuramérica; Portuguese: América do Sul; Quechua and Aymara: \nUrin Awya Yala; Guarani: Ñembyamérika; Dutch: Zuid-Amerika; \nFrench: Amérique du Sud) is a continent situated in the \nWestern Hemisphere, mostly in the Southern Hemisphere, with \na relatively small portion in the Northern Hemisphere. \nThe continent is also considered a subcontinent of the \nAmericas.[2][3] It is bordered on the west by the Pacific \nOcean and on the north and east by the Atlantic Ocean; \nNorth America and the Caribbean Sea lie to the northwest. \nIt includes twelve countries: Argentina, Bolivia, Brasil, \nChile, Colombia, Ecuador, Guyana, Paraguay, Peru, Suriname, \nUruguay, and Venezuela. The South American nations that \nborder the Caribbean Sea—including Colombia, Venezuela, \nGuyana, Suriname, as well as French Guiana, which is an \noverseas region of France—are a.k.a. Caribbean South \nAmerica. South America has an area of 17,840,000 square \nkilometers (6,890,000 sq mi). Its population as of 2005 \nhas been estimated at more than 371,090,000. South America \nranks fourth in area (after Asia, Africa, and North America) \nand fifth in population (after Asia, Africa, Europe, and \nNorth America). The word America was coined in 1507 by \ncartographers Martin Waldseemüller and Matthias Ringmann, \nafter Amerigo Vespucci, who was the first European to \nsuggest that the lands newly discovered by Europeans were \nnot India, but a New World unknown to Europeans.", 4 | "timezone": [ 5 | -4, 6 | -2 7 | ], 8 | "demographics": { 9 | "population": 385744896, 10 | "largestCities": [ 11 | "São Paulo", 12 | "Buenos Aires", 13 | "Rio de Janeiro", 14 | "Lima", 15 | "Bogotá" 16 | ] 17 | }, 18 | "languages": [ 19 | "spanish", 20 | "portuguese", 21 | "inglés", 22 | "dutch", 23 | "french", 24 | "quechua", 25 | "guaraní", 26 | "aimara", 27 | "mapudungun" 28 | ], 29 | "countries": [ 30 | { 31 | "name": "Argentina", 32 | "capital": "Rawson", 33 | "independence": "1816-07-08T12:20:56.000Z", 34 | "unasur": true 35 | }, 36 | { 37 | "name": "Bolivia", 38 | "capital": "La Paz", 39 | "independence": "1825-08-05T12:20:56.000Z", 40 | "unasur": true 41 | }, 42 | { 43 | "name": "Peru", 44 | "capital": "Lima", 45 | "independence": "1821-07-27T12:20:56.000Z", 46 | "unasur": true 47 | }, 48 | { 49 | "name": "Brazil", 50 | "capital": "Brasilia", 51 | "independence": "1822-09-06T12:20:56.000Z", 52 | "unasur": true 53 | }, 54 | { 55 | "name": "Chile", 56 | "capital": "Santiago", 57 | "independence": "1818-02-11T12:20:56.000Z", 58 | "unasur": true 59 | }, 60 | { 61 | "name": "Ecuador", 62 | "capital": "Quito", 63 | "independence": "1809-08-09T12:20:56.000Z", 64 | "unasur": true 65 | }, 66 | { 67 | "name": "Guyana", 68 | "capital": "Georgetown", 69 | "independence": "1966-05-25T12:00:00.000Z", 70 | "unasur": true 71 | }, 72 | { 73 | "name": "Paraguay", 74 | "capital": "Asunción", 75 | "independence": "1811-05-13T12:20:56.000Z", 76 | "unasur": true 77 | }, 78 | { 79 | "name": "Suriname", 80 | "capital": "Paramaribo", 81 | "independence": "1975-11-24T11:00:00.000Z", 82 | "unasur": true 83 | }, 84 | { 85 | "name": "Antártida", 86 | "unasur": false 87 | }, 88 | { 89 | "name": "Colombia", 90 | "capital": "Bogotá", 91 | "independence": "1810-07-19T12:20:56.000Z", 92 | "unasur": true, 93 | "population": 42888594 94 | } 95 | ], 96 | "spanishName": "Sudamérica" 97 | } -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.Benchmark/JsonHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Text; 4 | using System.Text.Json; 5 | using System.Text.Json.Nodes; 6 | 7 | namespace SystemTextJson.JsonDiffPatch.Benchmark 8 | { 9 | internal static class JsonHelper 10 | { 11 | public static JsonNode? Parse(string json) 12 | { 13 | using var document = JsonDocument.Parse(json); 14 | return CreateNode(document.RootElement); 15 | } 16 | 17 | private static JsonNode? CreateNode(in JsonElement element) 18 | { 19 | switch (element.ValueKind) 20 | { 21 | case JsonValueKind.Number: 22 | if (element.TryGetInt64(out var longValue)) 23 | return JsonValue.Create(longValue); 24 | if (element.TryGetDecimal(out var decimalValue)) 25 | return JsonValue.Create(decimalValue); 26 | if (element.TryGetDouble(out var doubleValue)) 27 | return JsonValue.Create(doubleValue); 28 | 29 | break; 30 | 31 | case JsonValueKind.String: 32 | if (element.TryGetDateTimeOffset(out var dateTimeOffsetValue)) 33 | return JsonValue.Create(dateTimeOffsetValue); 34 | if (element.TryGetDateTime(out var dateTimeValue)) 35 | return JsonValue.Create(dateTimeValue); 36 | if (element.TryGetGuid(out var guidValue)) 37 | return JsonValue.Create(guidValue); 38 | 39 | return JsonValue.Create(element.GetString()); 40 | 41 | case JsonValueKind.True: 42 | case JsonValueKind.False: 43 | return JsonValue.Create(element.ValueKind == JsonValueKind.True); 44 | 45 | case JsonValueKind.Null: 46 | return null; 47 | 48 | case JsonValueKind.Array: 49 | var jsonArray = new JsonArray(); 50 | 51 | foreach (var itemElement in element.EnumerateArray()) 52 | { 53 | jsonArray.Add(CreateNode(itemElement)); 54 | } 55 | 56 | return jsonArray; 57 | 58 | case JsonValueKind.Object: 59 | var jsonObj = new JsonObject(); 60 | 61 | foreach (var prop in element.EnumerateObject()) 62 | { 63 | jsonObj.Add(prop.Name, CreateNode(prop.Value)); 64 | } 65 | 66 | return jsonObj; 67 | } 68 | 69 | throw new ArgumentException("Cannot parse JSON element."); 70 | } 71 | 72 | #if NET 73 | public static JsonElementWrapper ParseElement(string json) 74 | { 75 | var jsonChars = json.AsSpan(); 76 | var byteCount = Encoding.UTF8.GetByteCount(jsonChars); 77 | var buffer = ArrayPool.Shared.Rent(byteCount); 78 | var length = Encoding.UTF8.GetBytes(jsonChars, buffer); 79 | var reader = new Utf8JsonReader(buffer.AsSpan().Slice(0, length)); 80 | return new JsonElementWrapper 81 | { 82 | Value = JsonElement.ParseValue(ref reader), 83 | Buffer = buffer 84 | }; 85 | } 86 | 87 | public ref struct JsonElementWrapper 88 | { 89 | public JsonElement Value; 90 | public byte[] Buffer; 91 | 92 | public void Dispose() 93 | { 94 | ArrayPool.Shared.Return(Buffer, true); 95 | } 96 | } 97 | #endif 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Patching/JsonDiffPatcher.Object.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch.Diffs; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace System.Text.Json.JsonDiffPatch 5 | { 6 | static partial class JsonDiffPatcher 7 | { 8 | private static void PatchObject(JsonObject left, JsonObject patch, JsonPatchOptions options) 9 | { 10 | // When make changes in this method, also copy the changes to ReversePatch* method 11 | 12 | foreach (var prop in patch) 13 | { 14 | var innerPatch = prop.Value; 15 | if (innerPatch is null) 16 | { 17 | continue; 18 | } 19 | 20 | var propertyName = prop.Key; 21 | var propPatch = new JsonDiffDelta(innerPatch); 22 | var kind = propPatch.Kind; 23 | 24 | if (kind == DeltaKind.Added) 25 | { 26 | if (left.ContainsKey(propertyName)) 27 | { 28 | left.Remove(propertyName); 29 | } 30 | 31 | left.Add(propertyName, propPatch.GetAdded()); 32 | } 33 | else if (kind == DeltaKind.Deleted) 34 | { 35 | if (left.ContainsKey(propertyName)) 36 | { 37 | left.Remove(propertyName); 38 | } 39 | } 40 | else 41 | { 42 | if (left.TryGetPropertyValue(propertyName, out var value)) 43 | { 44 | var oldValue = value; 45 | Patch(ref value, innerPatch, options); 46 | if (!ReferenceEquals(oldValue, value)) 47 | { 48 | left[propertyName] = value; 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | private static void ReversePatchObject(JsonObject left, JsonObject patch, JsonReversePatchOptions options) 56 | { 57 | // When make changes in this method, also copy the changes to Patch* method 58 | 59 | foreach (var prop in patch) 60 | { 61 | var innerPatch = prop.Value; 62 | if (innerPatch is null) 63 | { 64 | continue; 65 | } 66 | 67 | var propertyName = prop.Key; 68 | var propPatch = new JsonDiffDelta(innerPatch); 69 | var kind = propPatch.Kind; 70 | 71 | if (kind == DeltaKind.Added) 72 | { 73 | if (left.ContainsKey(propertyName)) 74 | { 75 | left.Remove(propertyName); 76 | } 77 | } 78 | else if (kind == DeltaKind.Deleted) 79 | { 80 | if (left.ContainsKey(propertyName)) 81 | { 82 | left.Remove(propertyName); 83 | } 84 | 85 | left.Add(propertyName, propPatch.GetDeleted()); 86 | } 87 | else 88 | { 89 | if (left.TryGetPropertyValue(propertyName, out var value)) 90 | { 91 | var oldValue = value; 92 | ReversePatch(ref value, innerPatch, options); 93 | if (!ReferenceEquals(oldValue, value)) 94 | { 95 | left[propertyName] = value; 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test/Examples/demo_left.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "South America", 3 | "summary": "South America (Spanish: América del Sur, Sudamérica or \nSuramérica; Portuguese: América do Sul; Quechua and Aymara: \nUrin Awya Yala; Guarani: Ñembyamérika; Dutch: Zuid-Amerika; \nFrench: Amérique du Sud) is a continent situated in the \nWestern Hemisphere, mostly in the Southern Hemisphere, with \na relatively small portion in the Northern Hemisphere. \nThe continent is also considered a subcontinent of the \nAmericas.[2][3] It is bordered on the west by the Pacific \nOcean and on the north and east by the Atlantic Ocean; \nNorth America and the Caribbean Sea lie to the northwest. \nIt includes twelve countries: Argentina, Bolivia, Brazil, \nChile, Colombia, Ecuador, Guyana, Paraguay, Peru, Suriname, \nUruguay, and Venezuela. The South American nations that \nborder the Caribbean Sea—including Colombia, Venezuela, \nGuyana, Suriname, as well as French Guiana, which is an \noverseas region of France—are also known as Caribbean South \nAmerica. South America has an area of 17,840,000 square \nkilometers (6,890,000 sq mi). Its population as of 2005 \nhas been estimated at more than 371,090,000. South America \nranks fourth in area (after Asia, Africa, and North America) \nand fifth in population (after Asia, Africa, Europe, and \nNorth America). The word America was coined in 1507 by \ncartographers Martin Waldseemüller and Matthias Ringmann, \nafter Amerigo Vespucci, who was the first European to \nsuggest that the lands newly discovered by Europeans were \nnot India, but a New World unknown to Europeans.", 4 | "surface": 17840000, 5 | "timezone": [ 6 | -4, 7 | -2 8 | ], 9 | "demographics": { 10 | "population": 385742554, 11 | "largestCities": [ 12 | "São Paulo", 13 | "Buenos Aires", 14 | "Rio de Janeiro", 15 | "Lima", 16 | "Bogotá" 17 | ] 18 | }, 19 | "languages": [ 20 | "spanish", 21 | "portuguese", 22 | "english", 23 | "dutch", 24 | "french", 25 | "quechua", 26 | "guaraní", 27 | "aimara", 28 | "mapudungun" 29 | ], 30 | "countries": [ 31 | { 32 | "name": "Argentina", 33 | "capital": "Buenos Aires", 34 | "independence": "1816-07-08T12:20:56.000Z", 35 | "unasur": true 36 | }, 37 | { 38 | "name": "Bolivia", 39 | "capital": "La Paz", 40 | "independence": "1825-08-05T12:20:56.000Z", 41 | "unasur": true 42 | }, 43 | { 44 | "name": "Brazil", 45 | "capital": "Brasilia", 46 | "independence": "1822-09-06T12:20:56.000Z", 47 | "unasur": true 48 | }, 49 | { 50 | "name": "Chile", 51 | "capital": "Santiago", 52 | "independence": "1818-02-11T12:20:56.000Z", 53 | "unasur": true 54 | }, 55 | { 56 | "name": "Colombia", 57 | "capital": "Bogotá", 58 | "independence": "1810-07-19T12:20:56.000Z", 59 | "unasur": true 60 | }, 61 | { 62 | "name": "Ecuador", 63 | "capital": "Quito", 64 | "independence": "1809-08-09T12:20:56.000Z", 65 | "unasur": true 66 | }, 67 | { 68 | "name": "Guyana", 69 | "capital": "Georgetown", 70 | "independence": "1966-05-25T12:00:00.000Z", 71 | "unasur": true 72 | }, 73 | { 74 | "name": "Paraguay", 75 | "capital": "Asunción", 76 | "independence": "1811-05-13T12:20:56.000Z", 77 | "unasur": true 78 | }, 79 | { 80 | "name": "Peru", 81 | "capital": "Lima", 82 | "independence": "1821-07-27T12:20:56.000Z", 83 | "unasur": true 84 | }, 85 | { 86 | "name": "Suriname", 87 | "capital": "Paramaribo", 88 | "independence": "1975-11-24T11:00:00.000Z", 89 | "unasur": true 90 | }, 91 | { 92 | "name": "Uruguay", 93 | "capital": "Montevideo", 94 | "independence": "1825-08-24T12:20:56.000Z", 95 | "unasur": true 96 | }, 97 | { 98 | "name": "Venezuela", 99 | "capital": "Caracas", 100 | "independence": "1811-07-04T12:20:56.000Z", 101 | "unasur": true 102 | } 103 | ] 104 | } -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.NUnit.Tests/JsonAssertTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch.Nunit; 2 | using System.Text.Json.Nodes; 3 | using NUnit.Framework; 4 | 5 | namespace SystemTextJson.JsonDiffPatch.NUnit.Tests 6 | { 7 | [TestFixture] 8 | public class JsonAssertTests 9 | { 10 | [Test] 11 | public void AreEqual_String() 12 | { 13 | var json1 = "{\"foo\":\"bar\",\"baz\":\"qux\"}"; 14 | var json2 = "{\"baz\":\"qux\",\"foo\":\"bar\"}"; 15 | 16 | JsonAssert.AreEqual(json1, json2); 17 | } 18 | 19 | [Test] 20 | public void AreEqual_JsonNode() 21 | { 22 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 23 | var json2 = JsonNode.Parse("{\"baz\":\"qux\",\"foo\":\"bar\"}"); 24 | 25 | JsonAssert.AreEqual(json1, json2); 26 | } 27 | 28 | [Test] 29 | public void AreEqual_Nulls() 30 | { 31 | JsonNode? json1 = null; 32 | JsonNode? json2 = null; 33 | 34 | JsonAssert.AreEqual(json1, json2); 35 | } 36 | 37 | [Test] 38 | public void AreEqual_FailWithMessage() 39 | { 40 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 41 | var json2 = JsonNode.Parse("{\"foo\":\"baz\"}"); 42 | 43 | var error = Assert.Throws( 44 | () => JsonAssert.AreEqual(json1, json2)); 45 | 46 | StringAssert.Contains("JsonAssert.AreEqual() failure.", error!.Message); 47 | } 48 | 49 | [Test] 50 | public void AreEqual_FailWithDefaultOutput() 51 | { 52 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 53 | var json2 = JsonNode.Parse("{\"foo\":\"baz\"}"); 54 | 55 | var error = Assert.Throws( 56 | () => JsonAssert.AreEqual(json1, json2, true)); 57 | 58 | StringAssert.Contains("JsonAssert.AreEqual() failure.", error!.Message); 59 | StringAssert.Contains("Expected:", error.Message); 60 | StringAssert.Contains("Actual:", error.Message); 61 | StringAssert.Contains("Delta:", error.Message); 62 | } 63 | 64 | [Test] 65 | public void AreEqual_FailWithCustomOutput() 66 | { 67 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 68 | var json2 = JsonNode.Parse("{\"foo\":\"baz\"}"); 69 | 70 | var error = Assert.Throws(() => JsonAssert.AreEqual(json1, 71 | json2, _ => "Custom message")); 72 | 73 | StringAssert.Contains("JsonAssert.AreEqual() failure.", error!.Message); 74 | StringAssert.Contains("Custom message", error.Message); 75 | } 76 | 77 | [Test] 78 | public void AreNotEqual_String() 79 | { 80 | var json1 = "{\"foo\":\"bar\",\"baz\":\"qux\"}"; 81 | var json2 = "{\"foo\":\"baz\"}"; 82 | 83 | JsonAssert.AreNotEqual(json1, json2); 84 | } 85 | 86 | [Test] 87 | public void AreNotEqual_JsonNode() 88 | { 89 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 90 | var json2 = JsonNode.Parse("{\"foo\":\"baz\"}"); 91 | 92 | JsonAssert.AreNotEqual(json1, json2); 93 | } 94 | 95 | [Test] 96 | public void AreNotEqual_Nulls() 97 | { 98 | JsonNode? json1 = null; 99 | JsonNode? json2 = null; 100 | 101 | var error = Assert.Throws( 102 | () => JsonAssert.AreNotEqual(json1, json2)); 103 | 104 | Assert.IsNotNull(error); 105 | } 106 | 107 | [Test] 108 | public void AreNotEqual_FailWithMessage() 109 | { 110 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 111 | var json2 = JsonNode.Parse("{\"baz\":\"qux\",\"foo\":\"bar\"}"); 112 | 113 | var error = Assert.Throws( 114 | () => JsonAssert.AreNotEqual(json1, json2)); 115 | 116 | StringAssert.Contains("JsonAssert.AreNotEqual() failure.", error!.Message); 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Diffs/JsonDiffPatcher.Text.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch.Diffs; 2 | using System.Text.Json.Nodes; 3 | using System.Threading; 4 | using DiffMatchPatch; 5 | 6 | namespace System.Text.Json.JsonDiffPatch 7 | { 8 | static partial class JsonDiffPatcher 9 | { 10 | private const string TextPatchFailed = "Text diff patch failed."; 11 | 12 | private static readonly Lazy DefaultTextDiffAlgorithm = new( 13 | () => new diff_match_patch(), 14 | LazyThreadSafetyMode.ExecutionAndPublication); 15 | 16 | // Long text diff using custom algorithm or by default Google's diff-match-patch: 17 | // https://github.com/benjamine/jsondiffpatch/blob/master/docs/deltas.md#text-diffs 18 | private static void DiffLongText( 19 | ref JsonDiffDelta delta, 20 | string left, 21 | string right, 22 | JsonDiffOptions options) 23 | { 24 | var alg = options.TextDiffProvider ?? DefaultTextDiff; 25 | var diff = alg(left, right); 26 | if (diff is not null) 27 | { 28 | delta.Text(diff); 29 | } 30 | 31 | static string? DefaultTextDiff(string str1, string str2) 32 | { 33 | var alg = DefaultTextDiffAlgorithm.Value; 34 | var patches = alg.patch_make(str1, str2); 35 | if (patches.Count == 0) 36 | { 37 | return null; 38 | } 39 | 40 | var diff = alg.patch_toText(patches); 41 | return diff; 42 | } 43 | } 44 | 45 | private static bool IsLongText( 46 | JsonValue left, 47 | JsonValue right, 48 | JsonDiffOptions? options, 49 | out string? leftText, 50 | out string? rightText) 51 | { 52 | leftText = null; 53 | rightText = null; 54 | 55 | if (options is null) 56 | { 57 | return false; 58 | } 59 | 60 | while (true) 61 | { 62 | if (options.TextDiffMinLength <= 0) 63 | { 64 | break; 65 | } 66 | 67 | // Perf: This is slower than direct property access 68 | var valueLeft = left.GetValue(); 69 | var valueRight = right.GetValue(); 70 | 71 | if (valueLeft is JsonElement elementLeft && valueRight is JsonElement elementRight) 72 | { 73 | if (elementLeft.ValueKind != JsonValueKind.String 74 | || elementRight.ValueKind != JsonValueKind.String) 75 | { 76 | break; 77 | } 78 | 79 | if (elementLeft.TryGetDateTimeOffset(out _) 80 | || elementLeft.TryGetDateTime(out _) 81 | || elementLeft.TryGetGuid(out _) 82 | || elementRight.TryGetDateTimeOffset(out _) 83 | || elementRight.TryGetDateTime(out _) 84 | || elementRight.TryGetGuid(out _)) 85 | { 86 | // Not text values 87 | break; 88 | } 89 | 90 | leftText = elementLeft.GetString(); 91 | rightText = elementRight.GetString(); 92 | } 93 | else if (valueLeft is string strLeft && valueRight is string strRight) 94 | { 95 | leftText = strLeft; 96 | rightText = strRight; 97 | } 98 | 99 | if (leftText is not null && rightText is not null) 100 | { 101 | // Align with: 102 | // https://github.com/benjamine/jsondiffpatch/blob/a8cde4c666a8a25d09d8f216c7f19397f2e1b569/src/filters/texts.js#L68 103 | if (leftText.Length >= options.TextDiffMinLength 104 | && rightText.Length >= options.TextDiffMinLength) 105 | { 106 | return true; 107 | } 108 | } 109 | 110 | break; 111 | } 112 | 113 | // Make sure we clear texts before return 114 | leftText = null; 115 | rightText = null; 116 | return false; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.UnitTests/DeepEqualsTestData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace SystemTextJson.JsonDiffPatch.UnitTests 6 | { 7 | [SuppressMessage("ReSharper", "HeapView.BoxingAllocation")] 8 | public static class DeepEqualsTestData 9 | { 10 | public static IEnumerable RawTextEqual(Func jsonizer) 11 | { 12 | yield return new[] {jsonizer("true"), jsonizer("true"), true}; 13 | yield return new[] {jsonizer("true"), jsonizer("false"), false}; 14 | yield return new[] {jsonizer("\"2019-11-27\""), jsonizer("\"2019-11-27\""), true}; 15 | yield return new[] {jsonizer("\"2019-11-27\""), jsonizer("\"2019-11-27T00:00:00.000\""), false}; 16 | yield return new[] {jsonizer("\"2019-11-27\""), jsonizer("\"Shaun is a rabbit\""), false}; 17 | yield return new[] {jsonizer("1"), jsonizer("1"), true}; 18 | yield return new[] {jsonizer("1"), jsonizer("2"), false}; 19 | yield return new[] {jsonizer("1.0"), jsonizer("1"), false}; 20 | yield return new[] {jsonizer("1.12e1"), jsonizer("11.2"), false}; 21 | yield return new[] {jsonizer("-1"), jsonizer("-1"), true}; 22 | yield return new[] {jsonizer("-1"), jsonizer("-1.0"), false}; 23 | yield return new[] {jsonizer("-1.1e1"), jsonizer("-11"), false}; 24 | yield return new[] 25 | { 26 | jsonizer("\"9d423bba-b9a8-4d19-a39a-b421bed58e02\""), 27 | jsonizer("\"9d423bba-b9a8-4d19-a39a-b421bed58e02\""), 28 | true 29 | }; 30 | yield return new[] 31 | { 32 | jsonizer("\"9d423bba-b9a8-4d19-a39a-b421bed58e02\""), 33 | jsonizer("\"b8baf656-8e97-4694-ae1a-be35e3a86db5\""), 34 | false 35 | }; 36 | yield return new[] 37 | { 38 | jsonizer("\"9d423bba-b9a8-4d19-a39a-b421bed58e02\""), 39 | jsonizer("\"9D423BBA-B9A8-4D19-A39A-B421BED58E02\""), 40 | false 41 | }; 42 | yield return new[] {jsonizer("\"Shaun is a rabbit\""), jsonizer("\"Shaun is a rabbit\""), true}; 43 | yield return new[] {jsonizer("\"Shaun is a rabbit\""), jsonizer("\"Shawn is a rabbit\""), false}; 44 | yield return new[] {jsonizer("1"), jsonizer("\"Shaun is a rabbit\""), false}; 45 | } 46 | 47 | public static IEnumerable SemanticEqual(Func jsonizer) 48 | { 49 | yield return new[] {jsonizer("true"), jsonizer("true"), true}; 50 | yield return new[] {jsonizer("true"), jsonizer("false"), false}; 51 | yield return new[] {jsonizer("\"2019-11-27\""), jsonizer("\"2019-11-27\""), true}; 52 | yield return new[] {jsonizer("\"2019-11-27\""), jsonizer("\"2019-11-27T00:00:00.000\""), true}; 53 | yield return new[] {jsonizer("\"2019-11-27\""), jsonizer("\"Shaun is a rabbit\""), false}; 54 | yield return new[] {jsonizer("1"), jsonizer("1"), true}; 55 | yield return new[] {jsonizer("1"), jsonizer("2"), false}; 56 | yield return new[] {jsonizer("1.0"), jsonizer("1"), true}; 57 | yield return new[] {jsonizer("1.12e1"), jsonizer("11.2"), true}; 58 | yield return new[] {jsonizer("-1"), jsonizer("-1"), true}; 59 | yield return new[] {jsonizer("-1"), jsonizer("-1.0"), true}; 60 | yield return new[] {jsonizer("-1.1e1"), jsonizer("-11"), true}; 61 | yield return new[] 62 | { 63 | jsonizer("\"9d423bba-b9a8-4d19-a39a-b421bed58e02\""), 64 | jsonizer("\"9d423bba-b9a8-4d19-a39a-b421bed58e02\""), 65 | true 66 | }; 67 | yield return new[] 68 | { 69 | jsonizer("\"9d423bba-b9a8-4d19-a39a-b421bed58e02\""), 70 | jsonizer("\"b8baf656-8e97-4694-ae1a-be35e3a86db5\""), 71 | false 72 | }; 73 | yield return new[] 74 | { 75 | jsonizer("\"9d423bba-b9a8-4d19-a39a-b421bed58e02\""), 76 | jsonizer("\"9D423BBA-B9A8-4D19-A39A-B421BED58E02\""), 77 | true 78 | }; 79 | yield return new[] {jsonizer("\"Shaun is a rabbit\""), jsonizer("\"Shaun is a rabbit\""), true}; 80 | yield return new[] {jsonizer("\"Shaun is a rabbit\""), jsonizer("\"Shawn is a rabbit\""), false}; 81 | yield return new[] {jsonizer("1"), jsonizer("\"Shaun is a rabbit\""), false}; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.UnitTests/NodeTests/ObjectDeepEqualsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch; 2 | using System.Text.Json.Nodes; 3 | using Xunit; 4 | 5 | namespace SystemTextJson.JsonDiffPatch.UnitTests.NodeTests 6 | { 7 | public class ObjectDeepEqualsTests 8 | { 9 | [Fact] 10 | public void Default() 11 | { 12 | Assert.True(default(JsonNode).DeepEquals(default, default(JsonComparerOptions))); 13 | } 14 | 15 | [Fact] 16 | public void Object_Identical() 17 | { 18 | var json1 = new JsonObject 19 | { 20 | {"foo", "bar"}, 21 | {"baz", "qux"} 22 | }; 23 | var json2 = new JsonObject 24 | { 25 | {"foo", "bar"}, 26 | {"baz", "qux"} 27 | }; 28 | 29 | Assert.True(json1.DeepEquals(json2, default(JsonComparerOptions))); 30 | } 31 | 32 | [Fact] 33 | public void Object_PropertyOrdering() 34 | { 35 | var json1 = new JsonObject 36 | { 37 | {"foo", "bar"}, 38 | {"baz", "qux"} 39 | }; 40 | var json2 = new JsonObject 41 | { 42 | {"baz", "qux"}, 43 | {"foo", "bar"} 44 | }; 45 | 46 | Assert.True(json1.DeepEquals(json2, default(JsonComparerOptions))); 47 | } 48 | 49 | [Fact] 50 | public void Object_PropertyValue() 51 | { 52 | var json1 = new JsonObject 53 | { 54 | {"foo", "bar"}, 55 | {"baz", "qux"} 56 | }; 57 | var json2 = new JsonObject 58 | { 59 | {"foo", "bar"}, 60 | {"baz", "quz"} 61 | }; 62 | 63 | Assert.False(json1.DeepEquals(json2, default(JsonComparerOptions))); 64 | } 65 | 66 | [Fact] 67 | public void Object_MissingProperty() 68 | { 69 | var json1 = new JsonObject 70 | { 71 | {"foo", "bar"}, 72 | {"baz", "qux"} 73 | }; 74 | var json2 = new JsonObject 75 | { 76 | {"foo", "bar"} 77 | }; 78 | 79 | Assert.False(json1.DeepEquals(json2, default(JsonComparerOptions))); 80 | } 81 | 82 | [Fact] 83 | public void Object_ExtraProperty() 84 | { 85 | var json1 = new JsonObject 86 | { 87 | {"foo", "bar"} 88 | }; 89 | var json2 = new JsonObject 90 | { 91 | {"foo", "bar"}, 92 | {"baz", "qux"} 93 | }; 94 | 95 | Assert.False(json1.DeepEquals(json2, default(JsonComparerOptions))); 96 | } 97 | 98 | [Fact] 99 | public void Array_Identical() 100 | { 101 | var json1 = new JsonArray {1, 2, 3}; 102 | var json2 = new JsonArray {1, 2, 3}; 103 | 104 | Assert.True(json1.DeepEquals(json2, default(JsonComparerOptions))); 105 | } 106 | 107 | [Fact] 108 | public void Array_ItemOrdering() 109 | { 110 | var json1 = new JsonArray {1, 2, 3}; 111 | var json2 = new JsonArray {1, 3, 2}; 112 | 113 | Assert.False(json1.DeepEquals(json2, default(JsonComparerOptions))); 114 | } 115 | 116 | [Fact] 117 | public void Array_ItemValue() 118 | { 119 | var json1 = new JsonArray {1, 2, 3}; 120 | var json2 = new JsonArray {1, 2, 5}; 121 | 122 | Assert.False(json1.DeepEquals(json2, default(JsonComparerOptions))); 123 | } 124 | 125 | [Fact] 126 | public void Array_MissingItem() 127 | { 128 | var json1 = new JsonArray {1, 2, 3}; 129 | var json2 = new JsonArray {1, 2}; 130 | 131 | Assert.False(json1.DeepEquals(json2, default(JsonComparerOptions))); 132 | } 133 | 134 | [Fact] 135 | public void Array_ExtraItem() 136 | { 137 | var json1 = new JsonArray {1, 2}; 138 | var json2 = new JsonArray {1, 2, 3}; 139 | 140 | Assert.False(json1.DeepEquals(json2, default(JsonComparerOptions))); 141 | } 142 | 143 | [Theory] 144 | [MemberData(nameof(NodeTestData.ObjectSemanticEqual), MemberType = typeof(NodeTestData))] 145 | public void Value_ObjectSemanticEqual(JsonValue json1, JsonValue json2, bool expected) 146 | { 147 | Assert.Equal(expected, json1.DeepEquals(json2, default(JsonComparerOptions))); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.UnitTests/NodeTests/PatchTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.JsonDiffPatch; 3 | using System.Text.Json.Nodes; 4 | using Xunit; 5 | 6 | namespace SystemTextJson.JsonDiffPatch.UnitTests.NodeTests 7 | { 8 | public class PatchTests 9 | { 10 | [Fact] 11 | public void Patch_AddedValue() 12 | { 13 | var left = JsonNode.Parse("1"); 14 | var diff = JsonNode.Parse("[3]"); 15 | 16 | Assert.Throws( 17 | () => JsonDiffPatcher.Patch(ref left, diff)); 18 | } 19 | 20 | [Fact] 21 | public void Patch_ModifiedValue() 22 | { 23 | var left = JsonNode.Parse("1"); 24 | var diff = JsonNode.Parse("[1,3]"); 25 | 26 | JsonDiffPatcher.Patch(ref left, diff); 27 | 28 | Assert.Equal("3", left!.ToJsonString()); 29 | } 30 | 31 | [Fact] 32 | public void Patch_DeletedValue() 33 | { 34 | var left = JsonNode.Parse("1"); 35 | var diff = JsonNode.Parse("[1,0,0]"); 36 | 37 | Assert.Throws( 38 | () => JsonDiffPatcher.Patch(ref left, diff)); 39 | } 40 | 41 | [Fact] 42 | public void Patch_Object() 43 | { 44 | var left = JsonNode.Parse("{\"a\":{\"a2\":2,\"a3\":3}}"); 45 | var diff = JsonNode.Parse("{\"a\":{\"a1\":[1],\"a2\":[2,3],\"a3\":[3,0,0]},\"b\":[1]}"); 46 | var result = new JsonObject 47 | { 48 | { 49 | "a", new JsonObject 50 | { 51 | {"a2", 3}, 52 | {"a1", 1}, 53 | } 54 | }, 55 | {"b", 1} 56 | }; 57 | 58 | JsonDiffPatcher.Patch(ref left, diff); 59 | 60 | Assert.Equal(result.ToJsonString(), left!.ToJsonString()); 61 | } 62 | 63 | [Fact] 64 | public void Patch_Array() 65 | { 66 | var left = JsonNode.Parse("[1,2,3,4]"); 67 | var diff = JsonNode.Parse("{\"_t\":\"a\",\"_0\":[\"\",5,3],\"_1\":[2,0,0],\"0\":[6],\"1\":[3,5],\"3\":[3],\"4\":[2]}"); 68 | var result = new JsonArray(6,5,4,3,2,1); 69 | 70 | JsonDiffPatcher.Patch(ref left, diff); 71 | 72 | Assert.Equal(result.ToJsonString(), left!.ToJsonString()); 73 | } 74 | 75 | [Fact] 76 | public void ReversePatch_AddedValue() 77 | { 78 | var right = JsonNode.Parse("1"); 79 | var diff = JsonNode.Parse("[3]"); 80 | 81 | Assert.Throws( 82 | () => JsonDiffPatcher.ReversePatch(ref right, diff)); 83 | } 84 | 85 | [Fact] 86 | public void ReversePatch_ModifiedValue() 87 | { 88 | var right = JsonNode.Parse("3"); 89 | var diff = JsonNode.Parse("[1,3]"); 90 | 91 | JsonDiffPatcher.ReversePatch(ref right, diff); 92 | 93 | Assert.Equal("1", right!.ToJsonString()); 94 | } 95 | 96 | [Fact] 97 | public void ReversePatch_DeletedValue() 98 | { 99 | var right = JsonNode.Parse("1"); 100 | var diff = JsonNode.Parse("[1,0,0]"); 101 | 102 | Assert.Throws( 103 | () => JsonDiffPatcher.ReversePatch(ref right, diff)); 104 | } 105 | 106 | [Fact] 107 | public void ReversePatch_Object() 108 | { 109 | var right = JsonNode.Parse("{\"a\":{\"a2\":3,\"a1\":1},\"b\":1}"); 110 | var diff = JsonNode.Parse("{\"a\":{\"a1\":[1],\"a2\":[2,3],\"a3\":[3,0,0]},\"b\":[1]}"); 111 | var result = new JsonObject 112 | { 113 | { 114 | "a", 115 | new JsonObject 116 | { 117 | {"a2", 2}, 118 | {"a3", 3} 119 | } 120 | } 121 | }; 122 | 123 | JsonDiffPatcher.ReversePatch(ref right, diff); 124 | 125 | Assert.Equal(result.ToJsonString(), right!.ToJsonString()); 126 | } 127 | 128 | [Fact] 129 | public void ReversePatch_Array() 130 | { 131 | var right = JsonNode.Parse("[6,5,4,3,2,1]"); 132 | var diff = JsonNode.Parse("{\"_t\":\"a\",\"_0\":[\"\",5,3],\"_1\":[2,0,0],\"0\":[6],\"1\":[3,5],\"3\":[3],\"4\":[2]}"); 133 | var result = new JsonArray(1, 2, 3, 4); 134 | 135 | JsonDiffPatcher.ReversePatch(ref right, diff); 136 | 137 | Assert.Equal(result.ToJsonString(), right!.ToJsonString()); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.Xunit.Tests/JsonAssertTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch.Xunit; 2 | using System.Text.Json.Nodes; 3 | using Xunit; 4 | 5 | namespace SystemTextJson.JsonDiffPatch.Xunit.Tests 6 | { 7 | public class JsonAssertTests 8 | { 9 | [Fact] 10 | public void Equal_String() 11 | { 12 | var json1 = "{\"foo\":\"bar\",\"baz\":\"qux\"}"; 13 | var json2 = "{\"baz\":\"qux\",\"foo\":\"bar\"}"; 14 | 15 | JsonAssert.Equal(json1, json2); 16 | } 17 | 18 | [Fact] 19 | public void Equal_JsonNode() 20 | { 21 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 22 | var json2 = JsonNode.Parse("{\"baz\":\"qux\",\"foo\":\"bar\"}"); 23 | 24 | JsonAssert.Equal(json1, json2); 25 | } 26 | 27 | [Fact] 28 | public void Equal_ExtensionMethod() 29 | { 30 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 31 | var json2 = JsonNode.Parse("{\"baz\":\"qux\",\"foo\":\"bar\"}"); 32 | 33 | json1.ShouldEqual(json2); 34 | } 35 | 36 | [Fact] 37 | public void Equal_Nulls() 38 | { 39 | JsonNode? json1 = null; 40 | JsonNode? json2 = null; 41 | 42 | JsonAssert.Equal(json1, json2); 43 | } 44 | 45 | [Fact] 46 | public void Equal_FailWithMessage() 47 | { 48 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 49 | var json2 = JsonNode.Parse("{\"foo\":\"baz\"}"); 50 | 51 | var error = Record.Exception(() => json1.ShouldEqual(json2)); 52 | 53 | Assert.IsType(error); 54 | Assert.Contains("JsonAssert.Equal() failure.", error.Message); 55 | } 56 | 57 | [Fact] 58 | public void Equal_FailWithDefaultOutput() 59 | { 60 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 61 | var json2 = JsonNode.Parse("{\"foo\":\"baz\"}"); 62 | 63 | var error = Record.Exception(() => json1.ShouldEqual(json2, true)); 64 | 65 | Assert.IsType(error); 66 | Assert.Contains("JsonAssert.Equal() failure.", error.Message); 67 | Assert.Contains("Expected:", error.Message); 68 | Assert.Contains("Actual:", error.Message); 69 | Assert.Contains("Delta:", error.Message); 70 | } 71 | 72 | [Fact] 73 | public void Equal_FailWithCustomOutput() 74 | { 75 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 76 | var json2 = JsonNode.Parse("{\"foo\":\"baz\"}"); 77 | 78 | var error = Record.Exception(() => json1.ShouldEqual(json2, 79 | _ => "Custom message")); 80 | 81 | Assert.IsType(error); 82 | Assert.Contains("JsonAssert.Equal() failure.", error.Message); 83 | Assert.Contains("Custom message", error.Message); 84 | } 85 | 86 | [Fact] 87 | public void NotEqual_String() 88 | { 89 | var json1 = "{\"foo\":\"bar\",\"baz\":\"qux\"}"; 90 | var json2 = "{\"foo\":\"baz\"}"; 91 | 92 | JsonAssert.NotEqual(json1, json2); 93 | } 94 | 95 | [Fact] 96 | public void NotEqual_JsonNode() 97 | { 98 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 99 | var json2 = JsonNode.Parse("{\"foo\":\"baz\"}"); 100 | 101 | JsonAssert.NotEqual(json1, json2); 102 | } 103 | 104 | [Fact] 105 | public void NotEqual_ExtensionMethod() 106 | { 107 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 108 | var json2 = JsonNode.Parse("{\"foo\":\"baz\"}"); 109 | 110 | json1.ShouldNotEqual(json2); 111 | } 112 | 113 | [Fact] 114 | public void NotEqual_Nulls() 115 | { 116 | JsonNode? json1 = null; 117 | JsonNode? json2 = null; 118 | 119 | var error = Record.Exception(() => json1.ShouldNotEqual(json2)); 120 | 121 | Assert.NotNull(error); 122 | } 123 | 124 | [Fact] 125 | public void NotEqual_FailWithMessage() 126 | { 127 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 128 | var json2 = JsonNode.Parse("{\"baz\":\"qux\",\"foo\":\"bar\"}"); 129 | 130 | var error = Record.Exception(() => json1.ShouldNotEqual(json2)); 131 | 132 | Assert.IsType(error); 133 | Assert.Contains("JsonAssert.NotEqual() failure.", error.Message); 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/JsonValueWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | 3 | namespace System.Text.Json.JsonDiffPatch 4 | { 5 | /// 6 | /// Wrapper of for value comparison purpose. 7 | /// 8 | internal struct JsonValueWrapper 9 | { 10 | // Keep as fields to avoid copy 11 | public JsonNumber NumberValue; 12 | public JsonString StringValue; 13 | 14 | public JsonValueWrapper(JsonValue value) 15 | { 16 | Value = value; 17 | ValueKind = JsonValueKind.Undefined; 18 | StringValue = default; 19 | 20 | if (JsonNumber.TryGetJsonNumber(value, out NumberValue)) 21 | { 22 | ValueKind = JsonValueKind.Number; 23 | } 24 | else if (JsonString.TryGetJsonString(value, out StringValue)) 25 | { 26 | ValueKind = JsonValueKind.String; 27 | } 28 | else if (value.TryGetValue(out var booleanValue)) 29 | { 30 | ValueKind = booleanValue ? JsonValueKind.True : JsonValueKind.False; 31 | } 32 | } 33 | 34 | public JsonValueKind ValueKind { get; } 35 | public JsonValue Value { get; } 36 | 37 | public bool DeepEquals(ref JsonValueWrapper another, in JsonComparerOptions comparerOptions) 38 | { 39 | var valueComparer = comparerOptions.ValueComparer; 40 | if (valueComparer is not null) 41 | { 42 | var hash1 = valueComparer.GetHashCode(Value); 43 | var hash2 = valueComparer.GetHashCode(another.Value); 44 | 45 | if (hash1 != hash2) 46 | { 47 | return false; 48 | } 49 | 50 | return valueComparer.Equals(Value, another.Value); 51 | } 52 | 53 | return DeepEquals(ref another, comparerOptions.JsonElementComparison); 54 | } 55 | 56 | public bool DeepEquals(ref JsonValueWrapper another, JsonElementComparison jsonElementComparison) 57 | { 58 | if (ReferenceEquals(Value, another.Value)) 59 | { 60 | return true; 61 | } 62 | 63 | if (ValueKind != another.ValueKind) 64 | { 65 | return false; 66 | } 67 | 68 | switch (ValueKind) 69 | { 70 | case JsonValueKind.Number: 71 | if (jsonElementComparison is JsonElementComparison.RawText && 72 | NumberValue.HasElement && 73 | another.NumberValue.HasElement) 74 | { 75 | return NumberValue.RawTextEquals(ref another.NumberValue); 76 | } 77 | 78 | return NumberValue.CompareTo(ref another.NumberValue) == 0; 79 | 80 | case JsonValueKind.String: 81 | if (jsonElementComparison is JsonElementComparison.RawText && 82 | StringValue.HasElement && 83 | another.StringValue.HasElement) 84 | { 85 | return StringValue.ValueEquals(ref another.StringValue); 86 | } 87 | 88 | return StringValue.Equals(ref another.StringValue); 89 | 90 | case JsonValueKind.True: 91 | case JsonValueKind.False: 92 | return true; 93 | 94 | case JsonValueKind.Null: 95 | case JsonValueKind.Undefined: 96 | case JsonValueKind.Object: 97 | case JsonValueKind.Array: 98 | default: 99 | return Value.TryGetValue(out var objX) 100 | && another.Value.TryGetValue(out var objY) 101 | && Equals(objX, objY); 102 | } 103 | } 104 | 105 | public int CompareTo(ref JsonValueWrapper another) 106 | { 107 | if (ValueKind != another.ValueKind) 108 | { 109 | return -((int) ValueKind - (int) another.ValueKind); 110 | } 111 | 112 | switch (ValueKind) 113 | { 114 | case JsonValueKind.Number: 115 | return NumberValue.CompareTo(ref another.NumberValue); 116 | 117 | case JsonValueKind.String: 118 | return StringValue.CompareTo(ref another.StringValue); 119 | 120 | case JsonValueKind.True: 121 | case JsonValueKind.False: 122 | return 0; 123 | 124 | case JsonValueKind.Null: 125 | case JsonValueKind.Undefined: 126 | case JsonValueKind.Object: 127 | case JsonValueKind.Array: 128 | default: 129 | throw new ArgumentOutOfRangeException( 130 | nameof(ValueKind), $"Unexpected value kind {ValueKind:G}"); 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.MSTest.Tests/JsonAssertTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch.MsTest; 2 | using System.Text.Json.Nodes; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace SystemTextJson.JsonDiffPatch.MsTest.Tests 6 | { 7 | [TestClass] 8 | public class JsonAssertTests 9 | { 10 | [TestMethod] 11 | public void AreEqual_String() 12 | { 13 | var json1 = "{\"foo\":\"bar\",\"baz\":\"qux\"}"; 14 | var json2 = "{\"baz\":\"qux\",\"foo\":\"bar\"}"; 15 | 16 | JsonAssert.AreEqual(json1, json2); 17 | } 18 | 19 | [TestMethod] 20 | public void AreEqual_JsonNode() 21 | { 22 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 23 | var json2 = JsonNode.Parse("{\"baz\":\"qux\",\"foo\":\"bar\"}"); 24 | 25 | JsonAssert.AreEqual(json1, json2); 26 | } 27 | 28 | [TestMethod] 29 | public void AreEqual_ExtensionMethod() 30 | { 31 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 32 | var json2 = JsonNode.Parse("{\"baz\":\"qux\",\"foo\":\"bar\"}"); 33 | 34 | Assert.That.JsonAreEqual(json1, json2); 35 | } 36 | 37 | [TestMethod] 38 | public void AreEqual_Nulls() 39 | { 40 | JsonNode? json1 = null; 41 | JsonNode? json2 = null; 42 | 43 | JsonAssert.AreEqual(json1, json2); 44 | } 45 | 46 | [TestMethod] 47 | public void AreEqual_FailWithMessage() 48 | { 49 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 50 | var json2 = JsonNode.Parse("{\"foo\":\"baz\"}"); 51 | 52 | var error = Assert.ThrowsException( 53 | () => Assert.That.JsonAreEqual(json1, json2)); 54 | 55 | StringAssert.Contains(error.Message, "JsonAssert.AreEqual() failure."); 56 | } 57 | 58 | [TestMethod] 59 | public void AreEqual_FailWithDefaultOutput() 60 | { 61 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 62 | var json2 = JsonNode.Parse("{\"foo\":\"baz\"}"); 63 | 64 | var error = Assert.ThrowsException( 65 | () => Assert.That.JsonAreEqual(json1, json2, true)); 66 | 67 | StringAssert.Contains(error.Message, "JsonAssert.AreEqual() failure."); 68 | StringAssert.Contains(error.Message, "Expected:"); 69 | StringAssert.Contains(error.Message, "Actual:"); 70 | StringAssert.Contains(error.Message, "Delta:"); 71 | } 72 | 73 | [TestMethod] 74 | public void AreEqual_FailWithCustomOutput() 75 | { 76 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 77 | var json2 = JsonNode.Parse("{\"foo\":\"baz\"}"); 78 | 79 | var error = Assert.ThrowsException(() => Assert.That.JsonAreEqual(json1, 80 | json2, _ => "Custom message")); 81 | 82 | StringAssert.Contains(error.Message, "JsonAssert.AreEqual() failure."); 83 | StringAssert.Contains(error.Message, "Custom message"); 84 | } 85 | 86 | [TestMethod] 87 | public void AreNotEqual_String() 88 | { 89 | var json1 = "{\"foo\":\"bar\",\"baz\":\"qux\"}"; 90 | var json2 = "{\"foo\":\"baz\"}"; 91 | 92 | JsonAssert.AreNotEqual(json1, json2); 93 | } 94 | 95 | [TestMethod] 96 | public void AreNotEqual_JsonNode() 97 | { 98 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 99 | var json2 = JsonNode.Parse("{\"foo\":\"baz\"}"); 100 | 101 | JsonAssert.AreNotEqual(json1, json2); 102 | } 103 | 104 | [TestMethod] 105 | public void AreNotEqual_ExtensionMethod() 106 | { 107 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 108 | var json2 = JsonNode.Parse("{\"foo\":\"baz\"}"); 109 | 110 | Assert.That.JsonAreNotEqual(json1, json2); 111 | } 112 | 113 | [TestMethod] 114 | public void AreNotEqual_Nulls() 115 | { 116 | JsonNode? json1 = null; 117 | JsonNode? json2 = null; 118 | 119 | var error = Assert.ThrowsException( 120 | () => Assert.That.JsonAreNotEqual(json1, json2)); 121 | 122 | Assert.IsNotNull(error); 123 | } 124 | 125 | [TestMethod] 126 | public void AreNotEqual_FailWithMessage() 127 | { 128 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 129 | var json2 = JsonNode.Parse("{\"baz\":\"qux\",\"foo\":\"bar\"}"); 130 | 131 | var error = Assert.ThrowsException( 132 | () => Assert.That.JsonAreNotEqual(json1, json2)); 133 | 134 | StringAssert.Contains(error.Message, "JsonAssert.AreNotEqual() failure."); 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.UnitTests/DocumentTests/DeepEqualsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.JsonDiffPatch; 3 | using Xunit; 4 | 5 | namespace SystemTextJson.JsonDiffPatch.UnitTests.DocumentTests 6 | { 7 | public class DeepEqualsTests 8 | { 9 | [Fact] 10 | public void Default() 11 | { 12 | Assert.True(default(JsonDocument).DeepEquals(default)); 13 | } 14 | 15 | [Fact] 16 | public void Object_Identical() 17 | { 18 | using var json1 = JsonDocument.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 19 | using var json2 = JsonDocument.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 20 | 21 | Assert.True(json1.DeepEquals(json2)); 22 | } 23 | 24 | [Fact] 25 | public void Object_Whitespace() 26 | { 27 | using var json1 = JsonDocument.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 28 | using var json2 = JsonDocument.Parse("{ \"foo\": \"bar\", \"baz\":\"qux\" }"); 29 | 30 | Assert.True(json1.DeepEquals(json2)); 31 | } 32 | 33 | [Fact] 34 | public void Object_PropertyOrdering() 35 | { 36 | using var json1 = JsonDocument.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 37 | using var json2 = JsonDocument.Parse("{\"baz\":\"qux\",\"foo\":\"bar\"}"); 38 | 39 | Assert.True(json1.DeepEquals(json2)); 40 | } 41 | 42 | [Fact] 43 | public void Object_PropertyValue() 44 | { 45 | using var json1 = JsonDocument.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 46 | using var json2 = JsonDocument.Parse("{\"foo\":\"bar\",\"baz\":\"quz\"}"); 47 | 48 | Assert.False(json1.DeepEquals(json2)); 49 | } 50 | 51 | [Fact] 52 | public void Object_MissingProperty() 53 | { 54 | using var json1 = JsonDocument.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 55 | using var json2 = JsonDocument.Parse("{\"foo\":\"bar\"}"); 56 | 57 | Assert.False(json1.DeepEquals(json2)); 58 | } 59 | 60 | [Fact] 61 | public void Object_ExtraProperty() 62 | { 63 | using var json1 = JsonDocument.Parse("{\"foo\":\"bar\"}"); 64 | using var json2 = JsonDocument.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 65 | 66 | Assert.False(json1.DeepEquals(json2)); 67 | } 68 | 69 | [Fact] 70 | public void Array_Identical() 71 | { 72 | using var json1 = JsonDocument.Parse("[1,2,3]"); 73 | using var json2 = JsonDocument.Parse("[1,2,3]"); 74 | 75 | Assert.True(json1.DeepEquals(json2)); 76 | } 77 | 78 | [Fact] 79 | public void Array_Whitespace() 80 | { 81 | using var json1 = JsonDocument.Parse("[1,2,3]"); 82 | using var json2 = JsonDocument.Parse("[ 1, 2, 3 ]"); 83 | 84 | Assert.True(json1.DeepEquals(json2)); 85 | } 86 | 87 | [Fact] 88 | public void Array_ItemOrdering() 89 | { 90 | using var json1 = JsonDocument.Parse("[1,2,3]"); 91 | using var json2 = JsonDocument.Parse("[1,3,2]"); 92 | 93 | Assert.False(json1.DeepEquals(json2)); 94 | } 95 | 96 | [Fact] 97 | public void Array_ItemValue() 98 | { 99 | using var json1 = JsonDocument.Parse("[1,2,3]"); 100 | using var json2 = JsonDocument.Parse("[1,2,5]"); 101 | 102 | Assert.False(json1.DeepEquals(json2)); 103 | } 104 | 105 | [Fact] 106 | public void Array_MissingItem() 107 | { 108 | using var json1 = JsonDocument.Parse("[1,2,3]"); 109 | using var json2 = JsonDocument.Parse("[1,2]"); 110 | 111 | Assert.False(json1.DeepEquals(json2)); 112 | } 113 | 114 | [Fact] 115 | public void Array_ExtraItem() 116 | { 117 | using var json1 = JsonDocument.Parse("[1,2]"); 118 | using var json2 = JsonDocument.Parse("[1,2,3]"); 119 | 120 | Assert.False(json1.DeepEquals(json2)); 121 | } 122 | 123 | [Theory] 124 | [MemberData(nameof(DocumentTestData.RawTextEqual), MemberType = typeof(DocumentTestData))] 125 | public void Value_RawText(JsonDocument json1, JsonDocument json2, bool expected) 126 | { 127 | using (json1) 128 | { 129 | using (json2) 130 | { 131 | Assert.Equal(expected, json1.DeepEquals(json2)); 132 | } 133 | } 134 | } 135 | 136 | [Theory] 137 | [MemberData(nameof(DocumentTestData.SemanticEqual), MemberType = typeof(DocumentTestData))] 138 | public void Value_Semantic(JsonDocument json1, JsonDocument json2, bool expected) 139 | { 140 | using (json1) 141 | { 142 | using (json2) 143 | { 144 | Assert.Equal(expected, json1.DeepEquals(json2, JsonElementComparison.Semantic)); 145 | } 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.UnitTests/NodeTests/DiffTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch; 2 | using System.Text.Json.Nodes; 3 | using Xunit; 4 | 5 | namespace SystemTextJson.JsonDiffPatch.UnitTests.NodeTests 6 | { 7 | public class DiffTests 8 | { 9 | [Fact] 10 | public void Diff_Added() 11 | { 12 | var left = JsonNode.Parse("{}"); 13 | var right = JsonNode.Parse("{\"a\":1}"); 14 | 15 | var diff = left.Diff(right); 16 | 17 | Assert.Equal("{\"a\":[1]}", diff!.ToJsonString()); 18 | } 19 | 20 | [Fact] 21 | public void Diff_Modified() 22 | { 23 | var left = JsonNode.Parse("1"); 24 | var right = JsonNode.Parse("2"); 25 | 26 | var diff = left.Diff(right); 27 | 28 | Assert.Equal("[1,2]", diff!.ToJsonString()); 29 | } 30 | 31 | [Fact] 32 | public void Diff_Deleted() 33 | { 34 | var left = JsonNode.Parse("{\"a\":1}"); 35 | var right = JsonNode.Parse("{}"); 36 | 37 | var diff = left.Diff(right); 38 | 39 | Assert.Equal("{\"a\":[1,0,0]}", diff!.ToJsonString()); 40 | } 41 | 42 | [Fact] 43 | public void Diff_ArrayMove() 44 | { 45 | var left = JsonNode.Parse("[1,2,3]"); 46 | var right = JsonNode.Parse("[2,1,3]"); 47 | 48 | var diff = left.Diff(right); 49 | 50 | Assert.Equal("{\"_t\":\"a\",\"_0\":[\"\",1,3]}", diff!.ToJsonString()); 51 | } 52 | 53 | [Fact] 54 | public void Diff_NullProperty() 55 | { 56 | var left = JsonNode.Parse("{\"a\":1}"); 57 | var right = JsonNode.Parse("{\"a\":null}"); 58 | 59 | var diff = left.Diff(right); 60 | 61 | Assert.Equal("{\"a\":[1,null]}", diff!.ToJsonString()); 62 | } 63 | 64 | [Fact] 65 | public void Diff_NullArrayItem() 66 | { 67 | var left = JsonNode.Parse("[1]"); 68 | var right = JsonNode.Parse("[null]"); 69 | 70 | var diff = left.Diff(right); 71 | 72 | Assert.Equal("{\"_t\":\"a\",\"_0\":[1,0,0],\"0\":[null]}", diff!.ToJsonString()); 73 | } 74 | 75 | [Fact] 76 | public void PropertyFilter_LeftProperty() 77 | { 78 | var left = JsonNode.Parse("{\"a\":1}"); 79 | var right = JsonNode.Parse("{}"); 80 | 81 | var diff = left.Diff(right, new JsonDiffOptions 82 | { 83 | PropertyFilter = (prop, _) => !string.Equals(prop, "a") 84 | }); 85 | 86 | Assert.Null(diff); 87 | } 88 | 89 | [Fact] 90 | public void PropertyFilter_RightProperty() 91 | { 92 | var left = JsonNode.Parse("{}"); 93 | var right = JsonNode.Parse("{\"a\":1}"); 94 | 95 | var diff = left.Diff(right, new JsonDiffOptions 96 | { 97 | PropertyFilter = (prop, _) => !string.Equals(prop, "a") 98 | }); 99 | 100 | Assert.Null(diff); 101 | } 102 | 103 | [Fact] 104 | public void PropertyFilter_NestedProperty() 105 | { 106 | var left = JsonNode.Parse("{\"foo\":{\"bar\":{\"a\":1}}}"); 107 | var right = JsonNode.Parse("{\"foo\":{\"bar\":{\"a\":2}}}"); 108 | 109 | var diff = left.Diff(right, new JsonDiffOptions 110 | { 111 | PropertyFilter = (prop, _) => !string.Equals(prop, "a") 112 | }); 113 | 114 | Assert.Null(diff); 115 | } 116 | 117 | [Fact] 118 | public void PropertyFilter_ArrayItem() 119 | { 120 | var left = JsonNode.Parse("[{\"a\":1}]"); 121 | var right = JsonNode.Parse("[{\"a\":2}]"); 122 | 123 | var diff = left.Diff(right, new JsonDiffOptions 124 | { 125 | PropertyFilter = (prop, _) => !string.Equals(prop, "a") 126 | }); 127 | 128 | Assert.Null(diff); 129 | } 130 | 131 | [Fact] 132 | public void PropertyFilter_IgnoreByPath() 133 | { 134 | var left = JsonNode.Parse("{\"a\":{\"b\":{\"c\":1}}}"); 135 | var right = JsonNode.Parse("{\"a\":{\"b\":{\"c\":2}}}"); 136 | 137 | var diff = left.Diff(right, new JsonDiffOptions 138 | { 139 | PropertyFilter = (_, ctx) => 140 | ctx.Left().GetPath() != "$.a.b" && ctx.Right().GetPath() != "$.a.b" 141 | }); 142 | 143 | Assert.Null(diff); 144 | } 145 | 146 | [Fact] 147 | public void PropertyFilter_ArrayItem_ExplicitFallbackMatch() 148 | { 149 | var left = JsonNode.Parse("[{\"a\":1}]"); 150 | var right = JsonNode.Parse("[{\"a\":2}]"); 151 | 152 | var diff = left.Diff(right, new JsonDiffOptions 153 | { 154 | ArrayObjectItemMatchByPosition = true, 155 | PropertyFilter = (prop, _) => !string.Equals(prop, "a") 156 | }); 157 | 158 | Assert.Null(diff); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.UnitTests/ElementTests/DeepEqualsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.JsonDiffPatch; 3 | using Xunit; 4 | 5 | namespace SystemTextJson.JsonDiffPatch.UnitTests.ElementTests 6 | { 7 | public class DeepEqualsTests 8 | { 9 | [Fact] 10 | public void Default() 11 | { 12 | Assert.True(default(JsonElement).DeepEquals(default)); 13 | } 14 | 15 | [Fact] 16 | public void Object_Identical() 17 | { 18 | var json1 = JsonSerializer.Deserialize("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 19 | var json2 = JsonSerializer.Deserialize("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 20 | 21 | Assert.True(json1.DeepEquals(json2)); 22 | } 23 | 24 | [Fact] 25 | public void Object_Whitespace() 26 | { 27 | var json1 = JsonSerializer.Deserialize("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 28 | var json2 = JsonSerializer.Deserialize("{ \"foo\": \"bar\", \"baz\":\"qux\" }"); 29 | 30 | Assert.True(json1.DeepEquals(json2)); 31 | } 32 | 33 | [Fact] 34 | public void Object_PropertyOrdering() 35 | { 36 | var json1 = JsonSerializer.Deserialize("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 37 | var json2 = JsonSerializer.Deserialize("{\"baz\":\"qux\",\"foo\":\"bar\"}"); 38 | 39 | Assert.True(json1.DeepEquals(json2)); 40 | } 41 | 42 | [Fact] 43 | public void Object_PropertyValue() 44 | { 45 | var json1 = JsonSerializer.Deserialize("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 46 | var json2 = JsonSerializer.Deserialize("{\"foo\":\"bar\",\"baz\":\"quz\"}"); 47 | 48 | Assert.False(json1.DeepEquals(json2)); 49 | } 50 | 51 | [Fact] 52 | public void Object_MissingProperty() 53 | { 54 | var json1 = JsonSerializer.Deserialize("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 55 | var json2 = JsonSerializer.Deserialize("{\"foo\":\"bar\"}"); 56 | 57 | Assert.False(json1.DeepEquals(json2)); 58 | } 59 | 60 | [Fact] 61 | public void Object_ExtraProperty() 62 | { 63 | var json1 = JsonSerializer.Deserialize("{\"foo\":\"bar\"}"); 64 | var json2 = JsonSerializer.Deserialize("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 65 | 66 | Assert.False(json1.DeepEquals(json2)); 67 | } 68 | 69 | [Fact] 70 | public void Array_Identical() 71 | { 72 | var json1 = JsonSerializer.Deserialize("[1,2,3]"); 73 | var json2 = JsonSerializer.Deserialize("[1,2,3]"); 74 | 75 | Assert.True(json1.DeepEquals(json2)); 76 | } 77 | 78 | [Fact] 79 | public void Array_Whitespace() 80 | { 81 | var json1 = JsonSerializer.Deserialize("[1,2,3]"); 82 | var json2 = JsonSerializer.Deserialize("[ 1, 2, 3 ]"); 83 | 84 | Assert.True(json1.DeepEquals(json2)); 85 | } 86 | 87 | [Fact] 88 | public void Array_ItemOrdering() 89 | { 90 | var json1 = JsonSerializer.Deserialize("[1,2,3]"); 91 | var json2 = JsonSerializer.Deserialize("[1,3,2]"); 92 | 93 | Assert.False(json1.DeepEquals(json2)); 94 | } 95 | 96 | [Fact] 97 | public void Array_ItemValue() 98 | { 99 | var json1 = JsonSerializer.Deserialize("[1,2,3]"); 100 | var json2 = JsonSerializer.Deserialize("[1,2,5]"); 101 | 102 | Assert.False(json1.DeepEquals(json2)); 103 | } 104 | 105 | [Fact] 106 | public void Array_MissingItem() 107 | { 108 | var json1 = JsonSerializer.Deserialize("[1,2,3]"); 109 | var json2 = JsonSerializer.Deserialize("[1,2]"); 110 | 111 | Assert.False(json1.DeepEquals(json2)); 112 | } 113 | 114 | [Fact] 115 | public void Array_ExtraItem() 116 | { 117 | var json1 = JsonSerializer.Deserialize("[1,2]"); 118 | var json2 = JsonSerializer.Deserialize("[1,2,3]"); 119 | 120 | Assert.False(json1.DeepEquals(json2)); 121 | } 122 | 123 | [Theory] 124 | [MemberData(nameof(ElementTestData.RawTextEqual), MemberType = typeof(ElementTestData))] 125 | public void Value_RawText(JsonElement json1, JsonElement json2, bool expected) 126 | { 127 | Assert.Equal(expected, json1.DeepEquals(json2)); 128 | } 129 | 130 | [Theory] 131 | [MemberData(nameof(ElementTestData.SemanticEqual), MemberType = typeof(ElementTestData))] 132 | public void Value_Semantic(JsonElement json1, JsonElement json2, bool expected) 133 | { 134 | Assert.Equal(expected, json1.DeepEquals(json2, JsonElementComparison.Semantic)); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /test/Examples/demo_diff_notext.json: -------------------------------------------------------------------------------- 1 | { 2 | "summary": [ 3 | "South America (Spanish: Am\u00E9rica del Sur, Sudam\u00E9rica or \nSuram\u00E9rica; Portuguese: Am\u00E9rica do Sul; Quechua and Aymara: \nUrin Awya Yala; Guarani: \u00D1embyam\u00E9rika; Dutch: Zuid-Amerika; \nFrench: Am\u00E9rique du Sud) is a continent situated in the \nWestern Hemisphere, mostly in the Southern Hemisphere, with \na relatively small portion in the Northern Hemisphere. \nThe continent is also considered a subcontinent of the \nAmericas.[2][3] It is bordered on the west by the Pacific \nOcean and on the north and east by the Atlantic Ocean; \nNorth America and the Caribbean Sea lie to the northwest. \nIt includes twelve countries: Argentina, Bolivia, Brazil, \nChile, Colombia, Ecuador, Guyana, Paraguay, Peru, Suriname, \nUruguay, and Venezuela. The South American nations that \nborder the Caribbean Sea\u2014including Colombia, Venezuela, \nGuyana, Suriname, as well as French Guiana, which is an \noverseas region of France\u2014are also known as Caribbean South \nAmerica. South America has an area of 17,840,000 square \nkilometers (6,890,000 sq mi). Its population as of 2005 \nhas been estimated at more than 371,090,000. South America \nranks fourth in area (after Asia, Africa, and North America) \nand fifth in population (after Asia, Africa, Europe, and \nNorth America). The word America was coined in 1507 by \ncartographers Martin Waldseem\u00FCller and Matthias Ringmann, \nafter Amerigo Vespucci, who was the first European to \nsuggest that the lands newly discovered by Europeans were \nnot India, but a New World unknown to Europeans.", 4 | "South America (Spanish: Am\u00E9rica del Sur, Sudam\u00E9rica or \nSuram\u00E9rica; Portuguese: Am\u00E9rica do Sul; Quechua and Aymara: \nUrin Awya Yala; Guarani: \u00D1embyam\u00E9rika; Dutch: Zuid-Amerika; \nFrench: Am\u00E9rique du Sud) is a continent situated in the \nWestern Hemisphere, mostly in the Southern Hemisphere, with \na relatively small portion in the Northern Hemisphere. \nThe continent is also considered a subcontinent of the \nAmericas.[2][3] It is bordered on the west by the Pacific \nOcean and on the north and east by the Atlantic Ocean; \nNorth America and the Caribbean Sea lie to the northwest. \nIt includes twelve countries: Argentina, Bolivia, Brasil, \nChile, Colombia, Ecuador, Guyana, Paraguay, Peru, Suriname, \nUruguay, and Venezuela. The South American nations that \nborder the Caribbean Sea\u2014including Colombia, Venezuela, \nGuyana, Suriname, as well as French Guiana, which is an \noverseas region of France\u2014are a.k.a. Caribbean South \nAmerica. South America has an area of 17,840,000 square \nkilometers (6,890,000 sq mi). Its population as of 2005 \nhas been estimated at more than 371,090,000. South America \nranks fourth in area (after Asia, Africa, and North America) \nand fifth in population (after Asia, Africa, Europe, and \nNorth America). The word America was coined in 1507 by \ncartographers Martin Waldseem\u00FCller and Matthias Ringmann, \nafter Amerigo Vespucci, who was the first European to \nsuggest that the lands newly discovered by Europeans were \nnot India, but a New World unknown to Europeans." 5 | ], 6 | "surface": [ 7 | 17840000, 8 | 0, 9 | 0 10 | ], 11 | "demographics": { 12 | "population": [ 13 | 385742554, 14 | 385744896 15 | ] 16 | }, 17 | "languages": { 18 | "_t": "a", 19 | "_2": [ 20 | "english", 21 | 0, 22 | 0 23 | ], 24 | "2": [ 25 | "ingl\u00E9s" 26 | ] 27 | }, 28 | "countries": { 29 | "_t": "a", 30 | "_0": [ 31 | { 32 | "name": "Argentina", 33 | "capital": "Buenos Aires", 34 | "independence": "1816-07-08T12:20:56.000Z", 35 | "unasur": true 36 | }, 37 | 0, 38 | 0 39 | ], 40 | "_4": [ 41 | { 42 | "name": "Colombia", 43 | "capital": "Bogot\u00E1", 44 | "independence": "1810-07-19T12:20:56.000Z", 45 | "unasur": true 46 | }, 47 | 0, 48 | 0 49 | ], 50 | "_8": [ 51 | "", 52 | 2, 53 | 3 54 | ], 55 | "_10": [ 56 | { 57 | "name": "Uruguay", 58 | "capital": "Montevideo", 59 | "independence": "1825-08-24T12:20:56.000Z", 60 | "unasur": true 61 | }, 62 | 0, 63 | 0 64 | ], 65 | "_11": [ 66 | { 67 | "name": "Venezuela", 68 | "capital": "Caracas", 69 | "independence": "1811-07-04T12:20:56.000Z", 70 | "unasur": true 71 | }, 72 | 0, 73 | 0 74 | ], 75 | "0": [ 76 | { 77 | "name": "Argentina", 78 | "capital": "Rawson", 79 | "independence": "1816-07-08T12:20:56.000Z", 80 | "unasur": true 81 | } 82 | ], 83 | "9": [ 84 | { 85 | "name": "Ant\u00E1rtida", 86 | "unasur": false 87 | } 88 | ], 89 | "10": [ 90 | { 91 | "name": "Colombia", 92 | "capital": "Bogot\u00E1", 93 | "independence": "1810-07-19T12:20:56.000Z", 94 | "unasur": true, 95 | "population": 42888594 96 | } 97 | ] 98 | }, 99 | "spanishName": [ 100 | "Sudam\u00E9rica" 101 | ] 102 | } -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.UnitTests/NodeTests/ElementDeepEqualsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch; 2 | using System.Text.Json.Nodes; 3 | using Xunit; 4 | 5 | namespace SystemTextJson.JsonDiffPatch.UnitTests.NodeTests 6 | { 7 | public class ElementDeepEqualsTests 8 | { 9 | [Fact] 10 | public void Object_Identical() 11 | { 12 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 13 | var json2 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 14 | 15 | Assert.True(json1.DeepEquals(json2, default(JsonComparerOptions))); 16 | } 17 | 18 | [Fact] 19 | public void Object_Whitespace() 20 | { 21 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 22 | var json2 = JsonNode.Parse("{ \"foo\": \"bar\", \"baz\":\"qux\" }"); 23 | 24 | Assert.True(json1.DeepEquals(json2, default(JsonComparerOptions))); 25 | } 26 | 27 | [Fact] 28 | public void Object_PropertyOrdering() 29 | { 30 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 31 | var json2 = JsonNode.Parse("{\"baz\":\"qux\",\"foo\":\"bar\"}"); 32 | 33 | Assert.True(json1.DeepEquals(json2, default(JsonComparerOptions))); 34 | } 35 | 36 | [Fact] 37 | public void Object_PropertyValue() 38 | { 39 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 40 | var json2 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"quz\"}"); 41 | 42 | Assert.False(json1.DeepEquals(json2, default(JsonComparerOptions))); 43 | } 44 | 45 | [Fact] 46 | public void Object_MissingProperty() 47 | { 48 | var json1 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 49 | var json2 = JsonNode.Parse("{\"foo\":\"bar\"}"); 50 | 51 | Assert.False(json1.DeepEquals(json2, default(JsonComparerOptions))); 52 | } 53 | 54 | [Fact] 55 | public void Object_ExtraProperty() 56 | { 57 | var json1 = JsonNode.Parse("{\"foo\":\"bar\"}"); 58 | var json2 = JsonNode.Parse("{\"foo\":\"bar\",\"baz\":\"qux\"}"); 59 | 60 | Assert.False(json1.DeepEquals(json2, default(JsonComparerOptions))); 61 | } 62 | 63 | [Fact] 64 | public void Array_Identical() 65 | { 66 | var json1 = JsonNode.Parse("[1,2,3]"); 67 | var json2 = JsonNode.Parse("[1,2,3]"); 68 | 69 | Assert.True(json1.DeepEquals(json2, default(JsonComparerOptions))); 70 | } 71 | 72 | [Fact] 73 | public void Array_Whitespace() 74 | { 75 | var json1 = JsonNode.Parse("[1,2,3]"); 76 | var json2 = JsonNode.Parse("[ 1, 2, 3 ]"); 77 | 78 | Assert.True(json1.DeepEquals(json2, default(JsonComparerOptions))); 79 | } 80 | 81 | [Fact] 82 | public void Array_ItemOrdering() 83 | { 84 | var json1 = JsonNode.Parse("[1,2,3]"); 85 | var json2 = JsonNode.Parse("[1,3,2]"); 86 | 87 | Assert.False(json1.DeepEquals(json2, default(JsonComparerOptions))); 88 | } 89 | 90 | [Fact] 91 | public void Array_ItemValue() 92 | { 93 | var json1 = JsonNode.Parse("[1,2,3]"); 94 | var json2 = JsonNode.Parse("[1,2,5]"); 95 | 96 | Assert.False(json1.DeepEquals(json2, default(JsonComparerOptions))); 97 | } 98 | 99 | [Fact] 100 | public void Array_MissingItem() 101 | { 102 | var json1 = JsonNode.Parse("[1,2,3]"); 103 | var json2 = JsonNode.Parse("[1,2]"); 104 | 105 | Assert.False(json1.DeepEquals(json2, default(JsonComparerOptions))); 106 | } 107 | 108 | [Fact] 109 | public void Array_ExtraItem() 110 | { 111 | var json1 = JsonNode.Parse("[1,2]"); 112 | var json2 = JsonNode.Parse("[1,2,3]"); 113 | 114 | Assert.False(json1.DeepEquals(json2, default(JsonComparerOptions))); 115 | } 116 | 117 | [Theory] 118 | [MemberData(nameof(NodeTestData.ElementRawTextEqual), MemberType = typeof(NodeTestData))] 119 | public void Value_RawText(JsonValue json1, JsonValue json2, bool expected) 120 | { 121 | Assert.Equal(expected, json1.DeepEquals(json2, default(JsonComparerOptions))); 122 | } 123 | 124 | [Theory] 125 | [MemberData(nameof(NodeTestData.ElementSemanticEqual), MemberType = typeof(NodeTestData))] 126 | public void Value_Semantic(JsonValue json1, JsonValue json2, bool expected) 127 | { 128 | Assert.Equal(expected, json1.DeepEquals(json2, JsonElementComparison.Semantic)); 129 | } 130 | 131 | [Theory] 132 | [MemberData(nameof(NodeTestData.ElementObjectSemanticEqual), MemberType = typeof(NodeTestData))] 133 | public void Value_ElementObjectSemanticEqual(JsonValue json1, JsonValue json2, bool expected) 134 | { 135 | Assert.Equal(expected, json1.DeepEquals(json2, default(JsonComparerOptions))); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Patching/JsonDiffPatcher.Patch.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.JsonDiffPatch.Diffs; 2 | using System.Text.Json.Nodes; 3 | 4 | namespace System.Text.Json.JsonDiffPatch 5 | { 6 | static partial class JsonDiffPatcher 7 | { 8 | /// 9 | /// Applies changes in the patch document to the JSON object. 10 | /// 11 | /// The JSON object to which patch is applied. 12 | /// The patch document previously generated by Diff method. 13 | /// The patch options. 14 | public static void Patch(ref JsonNode? left, JsonNode? patch, JsonPatchOptions options = default) 15 | { 16 | // When make changes in this method, also copy the changes to ReversePatch method 17 | 18 | if (patch is null) 19 | { 20 | return; 21 | } 22 | 23 | var delta = new JsonDiffDelta(patch); 24 | var kind = delta.Kind; 25 | 26 | switch (kind) 27 | { 28 | case DeltaKind.Modified: 29 | left = delta.GetNewValue(); 30 | return; 31 | case DeltaKind.Text 32 | when left is JsonValue jsonValue 33 | && jsonValue.TryGetValue(out var text): 34 | left = PatchLongText(text, delta, options); 35 | return; 36 | case DeltaKind.Object when left is JsonObject jsonObj: 37 | PatchObject(jsonObj, patch.AsObject(), options); 38 | return; 39 | case DeltaKind.Array when left is JsonArray jsonArray: 40 | PatchArray(jsonArray, patch.AsObject(), options); 41 | return; 42 | default: 43 | throw new FormatException(JsonDiffDelta.InvalidPatchDocument); 44 | } 45 | } 46 | 47 | /// 48 | /// Creates a deep copy the JSON object and applies changes in the patch document to the copy. 49 | /// 50 | /// The JSON object. 51 | /// The patch document previously generated by Diff method. 52 | /// The patch options. 53 | public static JsonNode? PatchNew(this JsonNode? left, JsonNode? patch, JsonPatchOptions options = default) 54 | { 55 | var copy = left?.DeepClone(); 56 | Patch(ref copy, patch, options); 57 | return copy; 58 | } 59 | 60 | /// 61 | /// Reverses the changes made by a previous call to . 62 | /// 63 | /// The JSON object from which patch is reversed. 64 | /// The patch document previously generated by Diff method. 65 | /// The patch options. 66 | public static void ReversePatch(ref JsonNode? right, JsonNode? patch, JsonReversePatchOptions options = default) 67 | { 68 | // When make changes in this method, also copy the changes to Patch method 69 | 70 | if (patch is null) 71 | { 72 | return; 73 | } 74 | 75 | var delta = new JsonDiffDelta(patch); 76 | var kind = delta.Kind; 77 | 78 | switch (kind) 79 | { 80 | case DeltaKind.Modified: 81 | right = delta.GetOldValue(); 82 | return; 83 | case DeltaKind.Text 84 | when right is JsonValue jsonValue 85 | && jsonValue.TryGetValue(out var text): 86 | right = ReversePatchLongText(text, delta, options); 87 | return; 88 | case DeltaKind.Object when right is JsonObject jsonObj: 89 | ReversePatchObject(jsonObj, patch.AsObject(), options); 90 | return; 91 | case DeltaKind.Array when right is JsonArray jsonArray: 92 | ReversePatchArray(jsonArray, patch.AsObject(), options); 93 | return; 94 | default: 95 | throw new FormatException(JsonDiffDelta.InvalidPatchDocument); 96 | } 97 | } 98 | 99 | /// 100 | /// Creates a deep copy the JSON object and reverses the changes made by 101 | /// a previous call to from the copy. 102 | /// 103 | /// The JSON object. 104 | /// The patch document previously generated by Diff method. 105 | /// The patch options. 106 | public static JsonNode? ReversePatchNew(this JsonNode? right, JsonNode? patch, JsonReversePatchOptions options = default) 107 | { 108 | var copy = right?.DeepClone(); 109 | ReversePatch(ref copy, patch, options); 110 | return copy; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Benchmark.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | ## Hardware and Software 4 | 5 | ``` ini 6 | 7 | BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1645 (21H1/May2021Update) 8 | 11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores 9 | .NET SDK=6.0.200 10 | [Host] : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT 11 | Job-ILXIOY : .NET 6.0.2 (6.0.222.6406), X64 RyuJIT 12 | 13 | 14 | ``` 15 | 16 | ## Comparison Modes 17 | 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 | 22 | 23 | \* _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)._ 24 | 25 | ## System.Text.Json vs Newtonsoft Json 26 | 27 | ### Diff (including RFC JsonPatch) 28 | 29 | | Method | FileSize | Mean | Median | Min | Max | P80 | P95 | Allocated | 30 | |------------------- |--------- |------------:|------------:|------------:|------------:|------------:|------------:|----------:| 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 | 39 | 40 | ### DeepEquals 41 | 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 | 50 | 51 | ### Patch 52 | 53 | | Method | FileSize | Mean | Median | Min | Max | P80 | P95 | Allocated | 54 | |--------------- |--------- |------------:|------------:|------------:|------------:|------------:|------------:|----------:| 55 | | **SystemTextJson** | **Small** | **35.45 μs** | **35.42 μs** | **34.43 μs** | **36.97 μs** | **35.86 μs** | **36.52 μs** | **35 KB** | 56 | | JsonNet | Small | 95.50 μs | 95.35 μs | 94.14 μs | 97.36 μs | 96.28 μs | 96.70 μs | 162 KB | 57 | | **SystemTextJson** | **Large** | **1,945.77 μs** | **1,935.61 μs** | **1,799.91 μs** | **2,203.39 μs** | **2,047.02 μs** | **2,093.61 μs** | **1,732 KB** | 58 | | JsonNet | Large | 4,324.16 μs | 4,315.50 μs | 4,184.21 μs | 4,506.67 μs | 4,378.94 μs | 4,433.86 μs | 5,088 KB | 59 | 60 | ### DeepClone 61 | 62 | | Method | FileSize | Mean | Median | Min | Max | P80 | P95 | Allocated | 63 | |--------------- |--------- |------------:|------------:|------------:|------------:|------------:|------------:|----------:| 64 | | **SystemTextJson** | **Small** | **28.98 μs** | **29.05 μs** | **27.99 μs** | **29.53 μs** | **29.29 μs** | **29.42 μs** | **40 KB** | 65 | | JsonNet | Small | 42.99 μs | 42.84 μs | 41.90 μs | 45.02 μs | 43.41 μs | 44.70 μs | 70 KB | 66 | | **SystemTextJson** | **Large** | **1,251.60 μs** | **1,247.97 μs** | **1,192.19 μs** | **1,323.97 μs** | **1,276.05 μs** | **1,310.40 μs** | **1,675 KB** | 67 | | JsonNet | Large | 1,708.43 μs | 1,706.69 μs | 1,664.39 μs | 1,783.04 μs | 1,731.47 μs | 1,759.00 μs | 2,128 KB | 68 | 69 | \* _All benchmarks for `SystemTextJson` methods are generated with `JsonElementComparison.Semantic` option and array move detection disabled because JsonDiffPatch.Net does not support array move detection._ -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Diffs/Formatters/DefaultDeltaFormatter.cs: -------------------------------------------------------------------------------- 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 : IJsonDiffDeltaFormatter 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(left), existingValue); 49 | break; 50 | case DeltaKind.Array: 51 | existingValue = FormatArray(ref delta, CheckType(left), existingValue); 52 | break; 53 | case DeltaKind.Object: 54 | existingValue = FormatObject(ref delta, CheckType(left), existingValue); 55 | break; 56 | } 57 | 58 | return existingValue; 59 | 60 | static T CheckType(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) 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 | } -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.UnitTests/DemoFileTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO; 3 | using System.Text.Json; 4 | using System.Text.Json.JsonDiffPatch; 5 | using System.Text.Json.JsonDiffPatch.Diffs.Formatters; 6 | using System.Text.Json.Nodes; 7 | using Xunit; 8 | using Xunit.Abstractions; 9 | 10 | namespace SystemTextJson.JsonDiffPatch.UnitTests 11 | { 12 | public class DemoFileTests 13 | { 14 | private readonly ITestOutputHelper _testOutputHelper; 15 | 16 | public DemoFileTests(ITestOutputHelper testOutputHelper) 17 | { 18 | _testOutputHelper = testOutputHelper; 19 | } 20 | 21 | [Fact] 22 | public void Diff_DemoJson() 23 | { 24 | // Compare the two JSON objects from https://benjamine.github.io/jsondiffpatch/demo/index.html 25 | var result = File.ReadAllText(@"Examples/demo_diff.json"); 26 | 27 | var sw = Stopwatch.StartNew(); 28 | var diff = JsonDiffPatcher.DiffFile( 29 | @"Examples/demo_left.json", 30 | @"Examples/demo_right.json", 31 | new JsonDiffOptions 32 | { 33 | TextDiffMinLength = 60, 34 | // https://github.com/benjamine/jsondiffpatch/blob/a8cde4c666a8a25d09d8f216c7f19397f2e1b569/docs/demo/demo.js#L163 35 | ArrayObjectItemKeyFinder = (n, i) => 36 | { 37 | if (n is JsonObject obj 38 | && obj.TryGetPropertyValue("name", out var value)) 39 | { 40 | return value?.GetValue() ?? ""; 41 | } 42 | 43 | return null; 44 | } 45 | }); 46 | sw.Stop(); 47 | 48 | var time = sw.ElapsedMilliseconds == 0 49 | ? $"{sw.ElapsedTicks} ticks" 50 | : $"{sw.Elapsed.TotalMilliseconds}ms"; 51 | _testOutputHelper.WriteLine($"Diff completed in {time}"); 52 | 53 | Assert.NotNull(diff); 54 | 55 | var diffJson = JsonSerializer.Serialize(diff, new JsonSerializerOptions 56 | { 57 | WriteIndented = true 58 | }); 59 | 60 | var resultDiff = JsonDiffPatcher.Diff(diffJson, result); 61 | 62 | Assert.Null(resultDiff); 63 | } 64 | 65 | [Fact] 66 | public void Roundtrip_DemoFile() 67 | { 68 | var diffOptions = new JsonDiffOptions {TextDiffMinLength = 60}; 69 | var left = JsonNode.Parse(File.ReadAllText(@"Examples/demo_left.json")); 70 | var originalLeft = JsonNode.Parse(File.ReadAllText(@"Examples/demo_left.json")); 71 | var right = JsonNode.Parse(File.ReadAllText(@"Examples/demo_right.json")); 72 | var diff = left.Diff(right, diffOptions); 73 | 74 | Assert.Null(left.Diff(originalLeft, diffOptions)); 75 | 76 | JsonDiffPatcher.Patch(ref left, diff); 77 | Assert.True(left.DeepEquals(right, default(JsonComparerOptions))); 78 | 79 | JsonDiffPatcher.ReversePatch(ref left, diff); 80 | Assert.True(left.DeepEquals(originalLeft, default(JsonComparerOptions))); 81 | } 82 | 83 | [Fact] 84 | public void Diff_DemoJson_JsonPatch() 85 | { 86 | var expectedDiff = JsonNode.Parse(File.ReadAllText(@"Examples/demo_diff_jsonpatch.json")); 87 | 88 | var diff = JsonDiffPatcher.DiffFile(@"Examples/demo_left.json", 89 | @"Examples/demo_right.json", 90 | new JsonPatchDeltaFormatter()); 91 | 92 | Assert.True(expectedDiff.DeepEquals(diff, default(JsonComparerOptions))); 93 | } 94 | 95 | [Fact] 96 | public void Diff_LargeObjects() 97 | { 98 | var diff = JsonDiffPatcher.DiffFile( 99 | @"Examples/large_left.json", 100 | @"Examples/large_right.json"); 101 | 102 | Assert.NotNull(diff); 103 | 104 | var diffJson = JsonSerializer.Serialize(diff, new JsonSerializerOptions 105 | { 106 | WriteIndented = true 107 | }); 108 | 109 | Assert.NotNull(diffJson); 110 | } 111 | 112 | [Fact] 113 | public void Roundtrip_LargeObjects() 114 | { 115 | var diffOptions = new JsonDiffOptions { TextDiffMinLength = 60 }; 116 | var left = JsonNode.Parse(File.ReadAllText(@"Examples/large_left.json")); 117 | var originalLeft = JsonNode.Parse(File.ReadAllText(@"Examples/large_left.json")); 118 | var right = JsonNode.Parse(File.ReadAllText(@"Examples/large_right.json")); 119 | var diff = left.Diff(right, diffOptions); 120 | 121 | Assert.Null(left.Diff(originalLeft, diffOptions)); 122 | 123 | JsonDiffPatcher.Patch(ref left, diff); 124 | Assert.True(left.DeepEquals(right, default(JsonComparerOptions))); 125 | 126 | JsonDiffPatcher.ReversePatch(ref left, diff); 127 | Assert.True(left.DeepEquals(originalLeft, default(JsonComparerOptions))); 128 | } 129 | 130 | [Fact] 131 | public void Diff_LargeObjects_JsonPatch() 132 | { 133 | var expectedDiff = JsonNode.Parse(File.ReadAllText(@"Examples/large_diff_jsonpatch.json")); 134 | 135 | var diff = JsonDiffPatcher.DiffFile(@"Examples/large_left.json", 136 | @"Examples/large_right.json", 137 | new JsonPatchDeltaFormatter()); 138 | 139 | Assert.True(expectedDiff.DeepEquals(diff, default(JsonComparerOptions))); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Diffs/Formatters/JsonPatchDeltaFormatter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | 3 | namespace System.Text.Json.JsonDiffPatch.Diffs.Formatters 4 | { 5 | /// 6 | /// Defines methods to format into RFC6902 Json Patch format. 7 | /// See . 8 | /// 9 | public class JsonPatchDeltaFormatter : DefaultDeltaFormatter 10 | { 11 | private const string PropertyNameOperation = "op"; 12 | private const string PropertyNamePath = "path"; 13 | private const string PropertyNameValue = "value"; 14 | 15 | private const string OperationNameAdd = "add"; 16 | private const string OperationNameRemove = "remove"; 17 | private const string OperationNameReplace = "replace"; 18 | 19 | public JsonPatchDeltaFormatter() 20 | : base(true) 21 | { 22 | PathBuilder = new(); 23 | } 24 | 25 | protected StringBuilder PathBuilder { get; } 26 | 27 | protected override JsonNode? CreateDefault() 28 | { 29 | return new JsonArray(); 30 | } 31 | 32 | protected override JsonNode? FormatArrayElement(in JsonDiffDelta.ArrayChangeEntry arrayChange, 33 | JsonNode? left, JsonNode? existingValue) 34 | { 35 | using var _ = new PropertyPathScope(PathBuilder, arrayChange.Index); 36 | return base.FormatArrayElement(arrayChange, left, existingValue); 37 | } 38 | 39 | protected override JsonNode? FormatObjectProperty(ref JsonDiffDelta delta, JsonNode? left, 40 | string propertyName, JsonNode? existingValue) 41 | { 42 | using var _ = new PropertyPathScope(PathBuilder, propertyName); 43 | return base.FormatObjectProperty(ref delta, left, propertyName, existingValue); 44 | } 45 | 46 | protected override JsonNode? FormatAdded(ref JsonDiffDelta delta, JsonNode? existingValue) 47 | { 48 | var op = new JsonObject 49 | { 50 | {PropertyNameOperation, OperationNameAdd}, 51 | {PropertyNamePath, PathBuilder.ToString()}, 52 | {PropertyNameValue, delta.GetAdded()} 53 | }; 54 | existingValue!.AsArray().Add(op); 55 | return existingValue; 56 | } 57 | 58 | protected override JsonNode? FormatModified(ref JsonDiffDelta delta, JsonNode? left, JsonNode? existingValue) 59 | { 60 | var op = new JsonObject 61 | { 62 | {PropertyNameOperation, OperationNameReplace}, 63 | {PropertyNamePath, PathBuilder.ToString()}, 64 | {PropertyNameValue, delta.GetNewValue()} 65 | }; 66 | existingValue!.AsArray().Add(op); 67 | return existingValue; 68 | } 69 | 70 | protected override JsonNode? FormatDeleted(ref JsonDiffDelta delta, JsonNode? left, JsonNode? existingValue) 71 | { 72 | var op = new JsonObject 73 | { 74 | {PropertyNameOperation, OperationNameRemove}, 75 | {PropertyNamePath, PathBuilder.ToString()} 76 | }; 77 | existingValue!.AsArray().Add(op); 78 | return existingValue; 79 | } 80 | 81 | protected override JsonNode? FormatArrayMove(ref JsonDiffDelta delta, JsonNode? left, JsonNode? existingValue) 82 | { 83 | // This should never happen. Array move operations should have been flattened into deletes and adds. 84 | throw new InvalidOperationException("Array move cannot be formatted."); 85 | } 86 | 87 | protected override JsonNode? FormatTextDiff(ref JsonDiffDelta delta, JsonValue? left, JsonNode? existingValue) 88 | { 89 | throw new NotSupportedException("Text diff is not supported by JsonPath."); 90 | } 91 | 92 | private readonly struct PropertyPathScope : IDisposable 93 | { 94 | private readonly StringBuilder _pathBuilder; 95 | private readonly int _startIndex; 96 | private readonly int _length; 97 | 98 | public PropertyPathScope(StringBuilder pathBuilder, string propertyName) 99 | { 100 | _pathBuilder = pathBuilder; 101 | _startIndex = pathBuilder.Length; 102 | pathBuilder.Append('/'); 103 | pathBuilder.Append(Escape(propertyName)); 104 | _length = pathBuilder.Length - _startIndex; 105 | } 106 | 107 | public PropertyPathScope(StringBuilder pathBuilder, int index) 108 | { 109 | _pathBuilder = pathBuilder; 110 | _startIndex = pathBuilder.Length; 111 | pathBuilder.Append('/'); 112 | pathBuilder.Append(index.ToString("D")); 113 | _length = pathBuilder.Length - _startIndex; 114 | } 115 | 116 | public void Dispose() 117 | { 118 | _pathBuilder.Remove(_startIndex, _length); 119 | } 120 | 121 | private static string Escape(string str) 122 | { 123 | // Escape Json Pointer as per https://datatracker.ietf.org/doc/html/rfc6901#section-3 124 | var sb = new StringBuilder(str); 125 | for (var i = 0; i < sb.Length; i++) 126 | { 127 | if (sb[i] == '/') 128 | { 129 | sb.Insert(i, '~'); 130 | sb[++i] = '1'; 131 | } 132 | else if (sb[i] == '~') 133 | { 134 | sb.Insert(i, '~'); 135 | sb[++i] = '0'; 136 | } 137 | } 138 | 139 | return sb.ToString(); 140 | } 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch.sln: -------------------------------------------------------------------------------- 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 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SystemTextJson.JsonDiffPatch.Xunit", "SystemTextJson.JsonDiffPatch.Xunit\SystemTextJson.JsonDiffPatch.Xunit.csproj", "{8CC0FE2E-297C-4427-8B66-B50E867823C3}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SystemTextJson.JsonDiffPatch.MSTest", "SystemTextJson.JsonDiffPatch.MSTest\SystemTextJson.JsonDiffPatch.MSTest.csproj", "{B2E4B8B6-9F55-4C99-8968-43144BEB809B}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SystemTextJson.JsonDiffPatch.Xunit.Tests", "..\test\SystemTextJson.JsonDiffPatch.Xunit.Tests\SystemTextJson.JsonDiffPatch.Xunit.Tests.csproj", "{5AEAC978-7DD8-43BA-880E-D5B8B27EF7E7}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SystemTextJson.JsonDiffPatch.MSTest.Tests", "..\test\SystemTextJson.JsonDiffPatch.MSTest.Tests\SystemTextJson.JsonDiffPatch.MSTest.Tests.csproj", "{B3AF3E1B-7FAE-4105-97DD-B7635CD2A6CD}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SystemTextJson.JsonDiffPatch.NUnit", "SystemTextJson.JsonDiffPatch.NUnit\SystemTextJson.JsonDiffPatch.NUnit.csproj", "{194DF788-2210-444E-826B-C3A6C10F14F7}" 23 | EndProject 24 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SystemTextJson.JsonDiffPatch.NUnit.Tests", "..\test\SystemTextJson.JsonDiffPatch.NUnit.Tests\SystemTextJson.JsonDiffPatch.NUnit.Tests.csproj", "{7FAB51DE-6ED6-4CCA-887C-DB8274237E92}" 25 | EndProject 26 | Global 27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 28 | Debug|Any CPU = Debug|Any CPU 29 | Release|Any CPU = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {2F8D59B0-53DE-4EE7-A704-D3F89A00D9DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {2F8D59B0-53DE-4EE7-A704-D3F89A00D9DC}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {2F8D59B0-53DE-4EE7-A704-D3F89A00D9DC}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {2F8D59B0-53DE-4EE7-A704-D3F89A00D9DC}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {832E60B1-0225-48B3-9AAD-638A0F5AA15E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {832E60B1-0225-48B3-9AAD-638A0F5AA15E}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {832E60B1-0225-48B3-9AAD-638A0F5AA15E}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {832E60B1-0225-48B3-9AAD-638A0F5AA15E}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {0F173CC9-EA04-4243-A506-E2C59C905A5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {0F173CC9-EA04-4243-A506-E2C59C905A5E}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {0F173CC9-EA04-4243-A506-E2C59C905A5E}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {0F173CC9-EA04-4243-A506-E2C59C905A5E}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {8CC0FE2E-297C-4427-8B66-B50E867823C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {8CC0FE2E-297C-4427-8B66-B50E867823C3}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {8CC0FE2E-297C-4427-8B66-B50E867823C3}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {8CC0FE2E-297C-4427-8B66-B50E867823C3}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {B2E4B8B6-9F55-4C99-8968-43144BEB809B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {B2E4B8B6-9F55-4C99-8968-43144BEB809B}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {B2E4B8B6-9F55-4C99-8968-43144BEB809B}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {B2E4B8B6-9F55-4C99-8968-43144BEB809B}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {5AEAC978-7DD8-43BA-880E-D5B8B27EF7E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {5AEAC978-7DD8-43BA-880E-D5B8B27EF7E7}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {5AEAC978-7DD8-43BA-880E-D5B8B27EF7E7}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {5AEAC978-7DD8-43BA-880E-D5B8B27EF7E7}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {B3AF3E1B-7FAE-4105-97DD-B7635CD2A6CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {B3AF3E1B-7FAE-4105-97DD-B7635CD2A6CD}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {B3AF3E1B-7FAE-4105-97DD-B7635CD2A6CD}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {B3AF3E1B-7FAE-4105-97DD-B7635CD2A6CD}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {194DF788-2210-444E-826B-C3A6C10F14F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {194DF788-2210-444E-826B-C3A6C10F14F7}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {194DF788-2210-444E-826B-C3A6C10F14F7}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {194DF788-2210-444E-826B-C3A6C10F14F7}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {7FAB51DE-6ED6-4CCA-887C-DB8274237E92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 65 | {7FAB51DE-6ED6-4CCA-887C-DB8274237E92}.Debug|Any CPU.Build.0 = Debug|Any CPU 66 | {7FAB51DE-6ED6-4CCA-887C-DB8274237E92}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {7FAB51DE-6ED6-4CCA-887C-DB8274237E92}.Release|Any CPU.Build.0 = Release|Any CPU 68 | EndGlobalSection 69 | GlobalSection(SolutionProperties) = preSolution 70 | HideSolutionNode = FALSE 71 | EndGlobalSection 72 | GlobalSection(NestedProjects) = preSolution 73 | {832E60B1-0225-48B3-9AAD-638A0F5AA15E} = {AE524BA2-D5E2-4D5F-BC95-9AA064D46234} 74 | {0F173CC9-EA04-4243-A506-E2C59C905A5E} = {AE524BA2-D5E2-4D5F-BC95-9AA064D46234} 75 | {5AEAC978-7DD8-43BA-880E-D5B8B27EF7E7} = {AE524BA2-D5E2-4D5F-BC95-9AA064D46234} 76 | {B3AF3E1B-7FAE-4105-97DD-B7635CD2A6CD} = {AE524BA2-D5E2-4D5F-BC95-9AA064D46234} 77 | {7FAB51DE-6ED6-4CCA-887C-DB8274237E92} = {AE524BA2-D5E2-4D5F-BC95-9AA064D46234} 78 | EndGlobalSection 79 | GlobalSection(ExtensibilityGlobals) = postSolution 80 | SolutionGuid = {F15DC234-2EF5-47F9-BB66-3C651311A3E5} 81 | EndGlobalSection 82 | EndGlobal 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SystemTextJson.JsonDiffPatch 2 | 3 | ![GitHub](https://img.shields.io/github/license/weichch/system-text-json-jsondiffpatch?color=blueviolet) ![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/weichch/system-text-json-jsondiffpatch/build-and-test.yaml?branch=main) [![JsonDiffPatch](https://img.shields.io/nuget/vpre/SystemTextJson.JsonDiffPatch?style=flat)](https://www.nuget.org/packages/SystemTextJson.JsonDiffPatch/) ![Nuget](https://img.shields.io/nuget/dt/SystemTextJson.JsonDiffPatch?color=important) 4 | 5 | High-performance, low-allocating JSON object diff and patch extension for System.Text.Json. 6 | 7 | ## Features 8 | 9 | - Compatible with [jsondiffpatch delta format](https://github.com/benjamine/jsondiffpatch/blob/master/docs/deltas.md) 10 | - Support generating patch document in RFC 6902 JSON Patch format 11 | - Support .NET and .NET Framework 12 | - Alternative to [jsondiffpatch.net](https://github.com/wbish/jsondiffpatch.net) which is based on Newtonsoft.Json 13 | - Fast large JSON document diffing with less memory consumption (see [benchmark](https://github.com/weichch/system-text-json-jsondiffpatch/blob/main/Benchmark.md)) 14 | - Support smart array diffing (e.g. move detect) using LCS (Longest Common Subsequence) and custom array item matcher 15 | - _(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 `DeepEquals` method for comparing `JsonDocument`, `JsonElement` and `JsonNode` 17 | - 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 18 | - JSON assert for xUnit, MSTest v2 and NUnit with customizable delta output 19 | 20 | ## Install 21 | 22 | #### JsonDiffPatch 23 | 24 | ``` 25 | PM> Install-Package SystemTextJson.JsonDiffPatch 26 | ``` 27 | 28 | #### xUnit Assert 29 | 30 | ``` 31 | PM> Install-Package SystemTextJson.JsonDiffPatch.Xunit 32 | ``` 33 | 34 | #### MSTest v2 Assert 35 | 36 | ``` 37 | PM> Install-Package SystemTextJson.JsonDiffPatch.MSTest 38 | ``` 39 | 40 | #### NUnit Assert 41 | 42 | ``` 43 | PM> Install-Package SystemTextJson.JsonDiffPatch.NUnit 44 | ``` 45 | 46 | ## Examples 47 | 48 | ### Diff 49 | 50 | ```csharp 51 | // Diff JsonNode 52 | var node1 = JsonNode.Parse("{\"foo\":\"bar\"}"); 53 | var node2 = JsonNode.Parse("{\"baz\":\"qux\", \"foo\":\"bar\"}"); 54 | var diff = node1.Diff(node2); 55 | // Diff with options 56 | var diff = node1.Diff(node2, new JsonDiffOptions 57 | { 58 | JsonElementComparison = JsonElementComparison.Semantic 59 | }); 60 | // Diff and convert delta into RFC 6902 JSON Patch format 61 | var diff = node1.Diff(node2, new JsonPatchDeltaFormatter()); 62 | // Diff JSON files 63 | var diff = JsonDiffPatcher.DiffFile(file1, file2); 64 | // Diff Span 65 | var diff = JsonDiffPatcher.Diff(span1, span2); 66 | // Diff streams 67 | var diff = JsonDiffPatcher.Diff(stream1, stream2); 68 | // Diff JSON strings 69 | var diff = JsonDiffPatcher.Diff(json1, json2); 70 | // Diff JSON readers 71 | var diff = JsonDiffPatcher.Diff(ref reader1, ref reader2); 72 | ``` 73 | 74 | ### Patch & Unpatch 75 | 76 | ```csharp 77 | var node1 = JsonNode.Parse("{\"foo\":\"bar\"}"); 78 | var node2 = JsonNode.Parse("{\"baz\":\"qux\", \"foo\":\"bar\"}"); 79 | var diff = node1.Diff(node2); 80 | // In-place patch 81 | JsonDiffPatcher.Patch(ref node1, diff); 82 | // Clone & patch 83 | var patched = node1.PatchNew(diff); 84 | // In-place unpatch 85 | JsonDiffPatcher.ReversePatch(ref node1, diff); 86 | // Clone & unpatch 87 | var patched = node1.ReversePatchNew(diff); 88 | ``` 89 | 90 | ### DeepEquals 91 | 92 | ```csharp 93 | // JsonDocument 94 | var doc1 = JsonDocument.Parse("{\"foo\":1}"); 95 | var doc2 = JsonDocument.Parse("{\"foo\":1.0}"); 96 | var equal = doc1.DeepEquals(doc2); 97 | var textEqual = doc1.DeepEquals(doc2, JsonElementComparison.RawText); 98 | var semanticEqual = doc1.DeepEquals(doc2, JsonElementComparison.Semantic); 99 | 100 | // JsonNode 101 | var node1 = JsonNode.Parse("{\"foo\":1}"); 102 | var node2 = JsonNode.Parse("{\"foo\":1.0}"); 103 | var equal = node1.DeepEquals(node2); 104 | var textEqual = node1.DeepEquals(node2, JsonElementComparison.RawText); 105 | var semanticEqual = node1.DeepEquals(node2, JsonElementComparison.Semantic); 106 | ``` 107 | 108 | ### Default Options 109 | 110 | ```csharp 111 | // Default diff options 112 | JsonDiffPatcher.DefaultOptions = () => new JsonDiffOptions 113 | { 114 | JsonElementComparison = JsonElementComparison.Semantic 115 | }; 116 | 117 | // Default comparison mode for DeepEquals 118 | JsonDiffPatcher.DefaultComparison = JsonElementComparison.Semantic; 119 | ``` 120 | 121 | ### Semantic Value Comparison 122 | ```csharp 123 | var node1 = JsonNode.Parse("\"2019-11-27\""); 124 | var node2 = JsonNode.Parse("\"2019-11-27T00:00:00.000\""); 125 | // dateCompare is 0 126 | var dateCompare = JsonValueComparer.Compare(node1, node2); 127 | 128 | var node3 = JsonNode.Parse("1"); 129 | var node4 = JsonNode.Parse("1.00"); 130 | // numCompare is 0 131 | var numCompare = JsonValueComparer.Compare(node3, node4); 132 | ``` 133 | 134 | ### Assert (Unit Testing) 135 | 136 | ```csharp 137 | var expected = JsonNode.Parse("{\"foo\":\"bar\"}"); 138 | var actual = JsonNode.Parse("{\"baz\":\"qux\", \"foo\":\"bar3\"}"); 139 | 140 | // xUnit 141 | JsonAssert.Equal(expected, actual); 142 | actual.ShouldEqual(expected); 143 | JsonAssert.NotEqual(expected, actual); 144 | actual.ShouldNotEqual(expected); 145 | 146 | // MSTest 147 | JsonAssert.AreEqual(expected, actual); 148 | Assert.That.JsonAreEqual(expected, actual); 149 | JsonAssert.AreNotEqual(expected, actual); 150 | Assert.That.JsonAreNotEqual(expected, actual); 151 | 152 | // NUnit 153 | JsonAssert.AreEqual(expected, actual); 154 | Assert.That(actual, JsonIs.EqualTo(expected)); 155 | JsonAssert.AreNotEqual(expected, actual); 156 | Assert.That(actual, JsonIs.NotEqualTo(expected)); 157 | ``` 158 | 159 | Example output _(when output is enabled)_: 160 | ``` 161 | JsonAssert.Equal() failure. 162 | Expected: 163 | { 164 | "foo": "bar" 165 | } 166 | Actual: 167 | { 168 | "baz": "qux", 169 | "foo": "bar" 170 | } 171 | Delta: 172 | { 173 | "foo": [ 174 | "bar", 175 | "bar3" 176 | ], 177 | "baz": [ 178 | "qux" 179 | ] 180 | } 181 | ``` 182 | 183 | ## Benchmark 184 | 185 | See detailed [benchmark results](https://github.com/weichch/system-text-json-jsondiffpatch/blob/main/Benchmark.md). 186 | -------------------------------------------------------------------------------- /test/SystemTextJson.JsonDiffPatch.UnitTests/FormatterTests/JsonPatchDeltaFormatterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.JsonDiffPatch; 3 | using System.Text.Json.JsonDiffPatch.Diffs.Formatters; 4 | using System.Text.Json.Nodes; 5 | using Xunit; 6 | 7 | namespace SystemTextJson.JsonDiffPatch.UnitTests.FormatterTests 8 | { 9 | public class JsonPatchDeltaFormatterTests 10 | { 11 | // Example test cases: https://datatracker.ietf.org/doc/html/rfc6902#appendix-A 12 | 13 | [Fact] 14 | public void IdenticalObjects_EmptyPatch() 15 | { 16 | var left = JsonNode.Parse("{\"foo\":\"bar\"}"); 17 | var right = JsonNode.Parse("{\"foo\":\"bar\"}"); 18 | 19 | var diff = left.Diff(right, new JsonPatchDeltaFormatter()); 20 | 21 | Assert.Equal("[]", diff!.ToJsonString()); 22 | } 23 | 24 | [Fact] 25 | public void Add_ObjectMember() 26 | { 27 | var left = JsonNode.Parse("{\"foo\":\"bar\"}"); 28 | var right = JsonNode.Parse("{\"baz\":\"qux\",\"foo\":\"bar\"}"); 29 | 30 | var diff = left.Diff(right, new JsonPatchDeltaFormatter()); 31 | 32 | Assert.Equal("[{\"op\":\"add\",\"path\":\"/baz\",\"value\":\"qux\"}]", diff!.ToJsonString()); 33 | } 34 | 35 | [Fact] 36 | public void Add_ArrayElement() 37 | { 38 | var left = JsonNode.Parse("{\"foo\":[\"bar\",\"baz\"]}"); 39 | var right = JsonNode.Parse("{\"foo\":[\"bar\",\"qux\",\"baz\"]}"); 40 | 41 | var diff = left.Diff(right, new JsonPatchDeltaFormatter()); 42 | 43 | Assert.Equal("[{\"op\":\"add\",\"path\":\"/foo/1\",\"value\":\"qux\"}]", diff!.ToJsonString()); 44 | } 45 | 46 | [Fact] 47 | public void Add_NestedObjectMember() 48 | { 49 | var left = JsonNode.Parse("{\"foo\":\"bar\"}"); 50 | var right = JsonNode.Parse("{\"foo\":\"bar\",\"child\":{\"grandchild\":{}}}"); 51 | 52 | var diff = left.Diff(right, new JsonPatchDeltaFormatter()); 53 | 54 | Assert.Equal("[{\"op\":\"add\",\"path\":\"/child\",\"value\":{\"grandchild\":{}}}]", diff!.ToJsonString()); 55 | } 56 | 57 | [Fact] 58 | public void Add_NestedArrayElement() 59 | { 60 | var left = JsonNode.Parse("{\"foo\":[\"bar\"]}"); 61 | var right = JsonNode.Parse("{\"foo\":[\"bar\",[\"abc\",\"def\"]]}"); 62 | 63 | var diff = left.Diff(right, new JsonPatchDeltaFormatter()); 64 | 65 | Assert.Equal("[{\"op\":\"add\",\"path\":\"/foo/1\",\"value\":[\"abc\",\"def\"]}]", diff!.ToJsonString()); 66 | } 67 | 68 | [Fact] 69 | public void Remove_ObjectMember() 70 | { 71 | var left = JsonNode.Parse("{\"baz\":\"qux\",\"foo\":\"bar\"}"); 72 | var right = JsonNode.Parse("{\"foo\":\"bar\"}"); 73 | 74 | var diff = left.Diff(right, new JsonPatchDeltaFormatter()); 75 | 76 | Assert.Equal("[{\"op\":\"remove\",\"path\":\"/baz\"}]", diff!.ToJsonString()); 77 | } 78 | 79 | [Fact] 80 | public void Remove_ArrayElement() 81 | { 82 | var left = JsonNode.Parse("{\"foo\":[\"bar\",\"qux\",\"baz\"]}"); 83 | var right = JsonNode.Parse("{\"foo\":[\"bar\",\"baz\"]}"); 84 | 85 | var diff = left.Diff(right, new JsonPatchDeltaFormatter()); 86 | 87 | Assert.Equal("[{\"op\":\"remove\",\"path\":\"/foo/1\"}]", diff!.ToJsonString()); 88 | } 89 | 90 | [Fact] 91 | public void Remove_OperationsOrder() 92 | { 93 | var left = JsonNode.Parse("[1,2]"); 94 | var right = JsonNode.Parse("[]"); 95 | 96 | var diff = left.Diff(right, new JsonPatchDeltaFormatter()); 97 | 98 | Assert.Equal( 99 | "[{\"op\":\"remove\",\"path\":\"/1\"},{\"op\":\"remove\",\"path\":\"/0\"}]", 100 | diff!.ToJsonString()); 101 | } 102 | 103 | [Fact] 104 | public void Replace_String() 105 | { 106 | var left = JsonNode.Parse("{\"baz\":\"qux\",\"foo\":\"bar\"}"); 107 | var right = JsonNode.Parse("{\"baz\":\"boo\",\"foo\":\"bar\"}"); 108 | 109 | var diff = left.Diff(right, new JsonPatchDeltaFormatter()); 110 | 111 | Assert.Equal("[{\"op\":\"replace\",\"path\":\"/baz\",\"value\":\"boo\"}]", diff!.ToJsonString()); 112 | } 113 | 114 | [Fact] 115 | public void Replace_OperationsOrder() 116 | { 117 | var left = JsonNode.Parse("[1,2]"); 118 | var right = JsonNode.Parse("[3,4]"); 119 | 120 | var diff = left.Diff(right, new JsonPatchDeltaFormatter()); 121 | 122 | Assert.Equal( 123 | "[{\"op\":\"remove\",\"path\":\"/1\"},{\"op\":\"remove\",\"path\":\"/0\"},{\"op\":\"add\",\"path\":\"/0\",\"value\":3},{\"op\":\"add\",\"path\":\"/1\",\"value\":4}]", 124 | diff!.ToJsonString()); 125 | } 126 | 127 | [Fact] 128 | public void Move_ArrayElement() 129 | { 130 | var left = JsonNode.Parse("{\"foo\":[\"all\",\"grass\",\"cows\",\"eat\"]}"); 131 | var right = JsonNode.Parse("{\"foo\":[\"all\",\"cows\",\"eat\",\"grass\"]}"); 132 | 133 | var diff = left.Diff(right, new JsonPatchDeltaFormatter()); 134 | 135 | Assert.Equal( 136 | "[{\"op\":\"remove\",\"path\":\"/foo/1\"},{\"op\":\"add\",\"path\":\"/foo/3\",\"value\":\"grass\"}]", 137 | diff!.ToJsonString()); 138 | } 139 | 140 | [Fact] 141 | public void Text_ThrowsNotSupportedException() 142 | { 143 | var left = JsonNode.Parse("{\"foo\":\"bar\"}"); 144 | var right = JsonNode.Parse("{\"foo\":\"boo\"}"); 145 | 146 | Assert.Throws(() => left.Diff(right, 147 | new JsonPatchDeltaFormatter(), 148 | new JsonDiffOptions 149 | { 150 | TextDiffMinLength = 1 151 | })); 152 | } 153 | 154 | [Fact] 155 | public void JsonPointer_EscapeSpecialChar() 156 | { 157 | var left = JsonNode.Parse("{\"data\":{\"/\":{\"~1\":1}}}"); 158 | var right = JsonNode.Parse("{\"data\":{\"/\":{\"~1\":2}}}"); 159 | 160 | var diff = left.Diff(right, new JsonPatchDeltaFormatter()); 161 | 162 | Assert.Equal("[{\"op\":\"replace\",\"path\":\"/data/~1/~01\",\"value\":2}]", diff!.ToJsonString()); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | # Rider 353 | .idea/ 354 | -------------------------------------------------------------------------------- /src/SystemTextJson.JsonDiffPatch/Diffs/Lcs.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using System.Text.Json.Nodes; 6 | 7 | namespace System.Text.Json.JsonDiffPatch.Diffs 8 | { 9 | internal readonly ref struct Lcs 10 | { 11 | private const int Equal = 1; 12 | private const int DeepEqual = 2; 13 | 14 | private readonly Dictionary? _lookupByLeftIndex; 15 | private readonly Dictionary? _lookupByRightIndex; 16 | private readonly int[]? _matrixRented; 17 | private readonly int[]? _matchMatrixRented; 18 | private readonly JsonValueWrapper[]? _wrapperCacheRented; 19 | private readonly int _rowSize; 20 | 21 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 22 | private Lcs(List indices, int[] matrixRented, int[] matchMatrixRented, 23 | JsonValueWrapper[]? wrapperCacheRented, int rowSize) 24 | { 25 | _lookupByLeftIndex = indices.ToDictionary(entry => entry.LeftIndex); 26 | _lookupByRightIndex = indices.ToDictionary(entry => entry.RightIndex); 27 | _matrixRented = matrixRented; 28 | _matchMatrixRented = matchMatrixRented; 29 | _wrapperCacheRented = wrapperCacheRented; 30 | _rowSize = rowSize; 31 | } 32 | 33 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 34 | public bool FindLeftIndex(int index, out LcsEntry result) 35 | { 36 | if (_lookupByLeftIndex is null) 37 | { 38 | result = default; 39 | return false; 40 | } 41 | 42 | return _lookupByLeftIndex.TryGetValue(index, out result); 43 | } 44 | 45 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 46 | public bool FindRightIndex(int index, out LcsEntry result) 47 | { 48 | if (_lookupByRightIndex is null) 49 | { 50 | result = default; 51 | return false; 52 | } 53 | 54 | return _lookupByRightIndex.TryGetValue(index, out result); 55 | } 56 | 57 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 58 | public bool AreEqual(int indexX, int indexY, out bool deepEqual) 59 | { 60 | if (_matchMatrixRented is null) 61 | { 62 | deepEqual = false; 63 | return false; 64 | } 65 | 66 | var matchResult = _matchMatrixRented[(indexX + 1) * _rowSize + indexY + 1]; 67 | deepEqual = matchResult == DeepEqual; 68 | return matchResult > 0; 69 | } 70 | 71 | public void Free() 72 | { 73 | if (_matrixRented is not null) 74 | { 75 | ArrayPool.Shared.Return(_matrixRented); 76 | } 77 | 78 | if (_matchMatrixRented is not null) 79 | { 80 | ArrayPool.Shared.Return(_matchMatrixRented); 81 | } 82 | 83 | if (_wrapperCacheRented is not null) 84 | { 85 | ArrayPool.Shared.Return(_wrapperCacheRented); 86 | } 87 | } 88 | 89 | public static Lcs Get(Span x, Span y, JsonDiffOptions? options) 90 | { 91 | if (x.Length == 0 || y.Length == 0) 92 | { 93 | // At least one sequence is empty 94 | return default; 95 | } 96 | 97 | var m = x.Length + 1; 98 | var n = y.Length + 1; 99 | var matrixLength = m * n; 100 | var matrixRented = ArrayPool.Shared.Rent(matrixLength); 101 | var matrix = matrixRented.AsSpan(0, matrixLength); 102 | var matchMatrixRented = ArrayPool.Shared.Rent(matrixLength); 103 | var matchMatrixSpan = matchMatrixRented.AsSpan(0, matrixLength); 104 | // For performance reasons, we set materialized values into a cache. 105 | // We only cache JSON values as they are more efficient to cache than objects and arrays. 106 | var wrapperCacheRented = ArrayPool.Shared.Rent(x.Length + y.Length); 107 | var wrapperCacheSpan = wrapperCacheRented.AsSpan(0, x.Length + y.Length); 108 | var comparerOptions = options?.CreateComparerOptions() ?? default; 109 | 110 | matrix.Fill(0); 111 | matchMatrixSpan.Fill(0); 112 | wrapperCacheSpan.Fill(default); 113 | 114 | for (var i = 1; i < m; i++) 115 | { 116 | if (x[i - 1] is JsonValue jsonValueX) 117 | { 118 | wrapperCacheSpan[i - 1] = new JsonValueWrapper(jsonValueX); 119 | } 120 | } 121 | 122 | for (var j = 1; j < n; j++) 123 | { 124 | if (y[j - 1] is JsonValue jsonValueY) 125 | { 126 | wrapperCacheSpan[x.Length + j - 1] = new JsonValueWrapper(jsonValueY); 127 | } 128 | } 129 | 130 | // Construct length matrix represented in a one-dimensional array using DP 131 | // https://en.wikipedia.org/wiki/Longest_common_subsequence_problem 132 | // https://www.geeksforgeeks.org/longest-common-subsequence-dp-4/ 133 | // 134 | // Given a matrix: 135 | // 136 | // Y 0 1 2 3 ... N 137 | // X 138 | // 0 139 | // 1 140 | // 2 141 | // . 142 | // . 143 | // M 144 | // 145 | // One-dimensional representation is: 146 | // [ [X=0, Y=[0 1 2 3 ... N]], [X=1, X=[0 1 2 3 ... N]], ..., [X=N, X=[0 1 2 3 ... N]] ] 147 | for (var i = 1; i < m; i++) 148 | { 149 | for (var j = 1; j < n; j++) 150 | { 151 | var matchContext = new ArrayItemMatchContext(x[i - 1], i - 1, y[j - 1], j - 1); 152 | bool itemMatched; 153 | 154 | if (x[i - 1] is JsonValue && y[j - 1] is JsonValue) 155 | { 156 | itemMatched = JsonDiffPatcher.MatchArrayValueItem(ref matchContext, 157 | ref wrapperCacheSpan[i - 1], 158 | ref wrapperCacheSpan[x.Length + j - 1], 159 | options, 160 | comparerOptions); 161 | } 162 | else 163 | { 164 | itemMatched = JsonDiffPatcher.MatchArrayItem(ref matchContext, options, comparerOptions); 165 | } 166 | 167 | if (itemMatched) 168 | { 169 | matrix[i * n + j] = 1 + matrix[(i - 1) * n + (j - 1)]; 170 | matchMatrixSpan[i * n + j] = matchContext.IsDeepEqual ? DeepEqual : Equal; 171 | } 172 | else 173 | { 174 | matrix[i * n + j] = Math.Max( 175 | // above 176 | matrix[(i - 1) * n + j], 177 | // left 178 | matrix[i * n + (j - 1)]); 179 | } 180 | } 181 | } 182 | 183 | // Backtrack 184 | if (matrix[matrixLength - 1] == 0) 185 | { 186 | // No common value 187 | return default; 188 | } 189 | 190 | var entries = new List(); 191 | for (int i = m - 1, j = n - 1; i > 0 && j > 0;) 192 | { 193 | if (matchMatrixSpan[i * n + j] > 0) 194 | { 195 | // X[i - 1] == Y [j - 1] 196 | entries.Insert(0, new LcsEntry(i - 1, j - 1, 197 | matchMatrixSpan[i * n + j] == DeepEqual)); 198 | i--; 199 | j--; 200 | } 201 | else 202 | { 203 | var valueAbove = matrix[(i - 1) * n + j]; 204 | var valueLeft = matrix[i * n + (j - 1)]; 205 | if (valueAbove > valueLeft) 206 | { 207 | // Move to above 208 | i--; 209 | } 210 | else 211 | { 212 | if (valueAbove == valueLeft) 213 | { 214 | var weightAbove = matchMatrixSpan[(i - 1) * n + j]; 215 | var weightLeft = matchMatrixSpan[(i - 1) * n + j]; 216 | if (weightAbove > weightLeft) 217 | { 218 | // Move to above, e.g. above was deeply equal 219 | i--; 220 | continue; 221 | } 222 | } 223 | 224 | // Move to left 225 | j--; 226 | } 227 | } 228 | } 229 | 230 | return new Lcs(entries, matrixRented, matchMatrixRented, wrapperCacheRented, n); 231 | } 232 | 233 | internal readonly struct LcsEntry 234 | { 235 | public LcsEntry(int leftIndex, int rightIndex, bool deepEqual) 236 | { 237 | LeftIndex = leftIndex; 238 | RightIndex = rightIndex; 239 | AreDeepEqual = deepEqual; 240 | } 241 | 242 | public readonly int LeftIndex; 243 | public readonly int RightIndex; 244 | public readonly bool AreDeepEqual; 245 | } 246 | } 247 | } 248 | --------------------------------------------------------------------------------