[C#] Json Compare/Diff 解決方案

現在的工作大都是使用微軟內建的 Json 序列化套件 System.Text.Json,為什麼要用可以參考 黑大這一篇,在尋求 Json Compare/Diff 解決方案時大都是看到 Newtonsoft(Json.NET) 的 JsonDiffPatch 做法,經同事分享 System.Text.Json 已經有人實作出來了,知道後立馬套用

開發環境

  • Windows 11
  • JetBrains Rider 2022.1.1
  • .NET 6

比對的功能主要有兩個方法, Diff 和 DeepEquals

  • Diff:列出差異
  • DeepEquals:回傳布林
    這可能不適合用在測試,因為他描述的問題不夠詳細,無法得知哪裡比對失敗

接下來我們就來看看 System.Text.Json 和 Json.NET 的用法吧

Newtonsoft(Json.NET)

原生的 Json.NET 已經有提供 DeepEquals 方法,JsonDiffPatch.Net 則是基於 Json.NET 發展的套件,擴充了 Diff 方法,開始之前請先安裝套件

Install-Package JsonDiffPatch.Net -Version 2.3.0
dotnet add package JsonDiffPatch.Net --version 2.3.0

它會依賴 Newtonsoft.Json

DeepEquals

下面則是使用 JToken.DeepEquals 的例子

[TestMethod]
public void 比對兩個一樣的JObject()
{
    JObject source = new JObject
    {
        { "Integer", 12345 },
        { "String", "A string" },
        { "Items", new JArray(1, 2) }
    };

    JObject dest = new JObject
    {
        { "Integer", 12345 },
        { "String", "A string" },
        { "Items", new JArray(1, 2) }
    };
    var isEquals = JToken.DeepEquals(source, dest);
    Assert.IsTrue(isEquals);
}

 

Diff

JsonDiffPatch 的使用方式如下:

[TestMethod]
public void 比對兩個不一樣的JObject()
{
    var source = new JObject
    {
        { "Integer", 12345 },
        { "String", "A string" },
        { "Items", new JArray(1, 2) }
    };

    var dest = new JObject
    {
        { "integer", 12345 },
        { "String", "A string" },
        { "Items", new JArray(1, 2, new JArray { "a", "b" }) }
    };
    var diffPath = new JsonDiffPatch();
    var diff = diffPath.Diff(source, dest);

    if (diff != null)
    {
        Console.WriteLine(diff.ToString());
    }
    
    Assert.IsNotNull(diff);
}

 

System.Text.Json.Json

.NET 6 替 System.Text.Json 新增 DOM 的控制物件:JsonNodeJsonArrayJsonObjectJsonValue  這可以更有效率的處理 Json 結構,如需詳細資訊,請參閱 JSON DOM 選項

System.Text.Json.JsonDiffPath 基於System.Text.Json.Json 套件的擴充,可以用它取代 jsondiffpatch.net 的功能。
JsonDocument、JsonElement、JsonNode 支援 DeepEquals 方法,也有 JsonAssert,讓我們在測試專案驗證。

更詳細的功能可以參考以下system-text-json-jsondiffpatch

 

開始之前請先安裝套件

Install-Package SystemTextJson.JsonDiffPatch.MSTest -Version 1.3.0
dotnet add package SystemTextJson.JsonDiffPatch.MSTest --version 1.3.0

它會依賴 SystemTextJson.JsonDiffPatch、System.Text.Json

DeepEquals

[TestMethod]
public void 比對兩個不一樣的JsonDocument()
{
    var source = new JsonObject
    {
        { "Integer", 12345 },
        { "String", JsonValue.Create("A string") },
        { "Items", new JsonArray(1, 2) }
    };

    var dest = new JsonObject
    {
        { "integer", 12345 },
        { "String", "A string" },
        { "Items", new JsonArray(1, 2, new JsonArray { "a", "b" }) }
    };

    var left = JsonDocument.Parse(source.ToJsonString());
    var right = JsonDocument.Parse(dest.ToJsonString());

    var isEquals = left.DeepEquals(right);
    Assert.IsFalse(isEquals);
}

Diff

JsonDiffPatcher

比對 Json 字串,通過 Diff 方法可以知道這兩個物件差異的結構在甚麼地方

[TestMethod]
public void 比對兩個一樣的Json字串_via_JsonDiffPatcher()
{
	var source = new JsonObject
	{
		{ "Integer", 12345 },
		{ "String", "A string" },
		{ "Items", new JsonArray(1, 2) }
	};

	var dest = new JsonObject
	{
		{ "Integer", 12345 },
		{ "String", "A string" },
		{ "Items", new JsonArray(1, 2) }
	};

	var left = source.ToJsonString();
	var right = dest.ToJsonString();
	var diff = JsonDiffPatcher.Diff(left, right);
	if (diff != null)
	{
		Console.WriteLine(JsonSerializer.Serialize(diff));
	}

	Assert.IsNull(diff);
}

 

除了Json 字串,還有很多型別,參考下圖

 

JsonObject
[TestMethod]
public void 比對兩個不一樣的JsonObject()
{
    var source = new JsonObject
    {
        { "Integer", 12345 },
        { "String", JsonValue.Create("A string") },
        { "Items", new JsonArray(1, 2) }
    };

    var dest = new JsonObject
    {
        { "integer", 12345 },
        { "String", "A string" },
        { "Items", new JsonArray(1, 2, new JsonArray { "a", "b" }) }
    };

    var diff = source.Diff(dest);
    if (diff != null)
    {
        Console.WriteLine(JsonSerializer.Serialize(diff));
    }

    Assert.IsNotNull(diff);
}

 

JsonNode
[TestMethod]
public void 比對兩個不一樣的JsonNode()
{
    var source = new JsonObject
    {
        { "Integer", 12345 },
        { "String", JsonValue.Create("A string") },
        { "Items", new JsonArray(1, 2) }
    };

    var dest = new JsonObject
    {
        { "integer", 12345 },
        { "String", "A string" },
        { "Items", new JsonArray(1, 2, new JsonArray { "a", "b" }) }
    };

    var left = JsonNode.Parse(source.ToJsonString());
    var right = JsonNode.Parse(dest.ToJsonString());
    var diff = left.Diff(right);
    if (diff != null)
    {
        Console.WriteLine(JsonSerializer.Serialize(diff));
    }

    Assert.IsNotNull(diff);
}

 

JsonAssert

上面的例子是先使用 Diff 方法知道哪裡不一樣,再把它們印出來,當我們在測試程式碼就可以直接使用組合技Assert.That.JsonAreEqual(o1, o2, true);,那個 true 的參數可以把不一樣的地方輸出到 Console

[TestMethod]
public void 比對兩個不一樣的JsonObject_via_JsonAssert()
{
    var source = new JsonObject
    {
        { "Integer", 12345 },
        { "String", JsonValue.Create("A string") },
        { "Items", new JsonArray(1, 2) }
    };

    var dest = new JsonObject
    {
        { "integer", 12345 },
        { "String", "A string" },
        { "Items", new JsonArray(1, 2, new JsonArray { "a", "b" }) }
    };
    Assert.That.JsonAreEqual(source, dest, true);
}

 

結論

System.Text.Json.JsonDiffPath 提供 DeepEquals 跟 Diff 兩個方法,DeepEquals 的效能肯定是優於 Diff 的,你得依據自己的需求來決定要使用哪一個,下圖出自:system-text-json-jsondiffpatch/Benchmark.md at main · weichch/system-text-json-jsondiffpatch (github.com)

System.Text.Json.JsonDiffPath 的擴充方法很明顯地優於 jsondiffpatch.net,開發體驗到目前為止感覺還不錯

範例位置

sample.dotblog/Json/Lab.JsonCompare at master · yaochangyu/sample.dotblog (github.com)

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo