我們最近有個專案需求特別複雜,由於是個已經持續維護 10 年的系統要改版,很多商業邏輯已經不可考,只能從程式碼中找尋蛛絲馬跡。不過,有些資料的欄位來自於程式碼,但更多來自於一組極其複雜的動態資料表設計。我們除了從現有的頁面上進行新系統設計外,有時候還會意外的多出幾個莫名的欄位,因此對於資料模型類別的規劃變的異常困難。本篇文章我將分享一種罕見的 Json.NET 資料序列化技巧,幫助你可以做到動態的 JSON 資料回應格式,同時又能保有強型別的設計。
建立 Console 應用程式
我打算用一個簡單的例子來說明這個過程,但事實上你在撰寫 Web API 的時候,只要是使用 Json.NET (Newtonsoft.Json) 來進行序列化/反序列化,其過程都是完全相同的!
-
我們先建立一個 .NET 5 的 Console 應用程式專案
mkdir c1 && cd c1
dotnet new globaljson --sdk-version 5.0.301
dotnet new console
dotnet add package Newtonsoft.Json
-
在專案中建立一個 sample.json
檔案,其內容如下
[
{
"_id": 959,
"date": "2021/1/1",
"name": "中華民國開國紀念日",
"description": "全國各機關學校放假一日",
"holidayCategory": "放假之紀念日及節日",
"isHoliday": "是"
},
{
"_id": 960,
"date": "2021/1/2",
"holidayCategory": "星期六、星期日",
"isHoliday": "是"
},
{
"_id": 961,
"date": "2021/1/3",
"holidayCategory": "星期六、星期日",
"isHoliday": "是"
}
]
你可以從上述資料看出,其實 JSON 資料中的 name
與 description
並不是每一筆資料都有!
-
建立一個名為 Holiday
的資料模型類別
using Newtonsoft.Json;
namespace c1
{
public partial class Holiday
{
[JsonProperty("_id")]
public long Id { get; set; }
[JsonProperty("date")]
public string Date { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("holidayCategory")]
public string HolidayCategory { get; set; }
[JsonProperty("isHoliday")]
public string IsHoliday { get; set; }
}
}
-
建立主程式
using System;
using System.IO;
using Newtonsoft.Json;
namespace c1
{
class Program
{
static void Main(string[] args)
{
var data = JsonConvert.DeserializeObject<Holiday[]>(File.ReadAllText("./sample.json"));
foreach (var item in data)
{
Console.WriteLine(item.Id + "\t" + item.Date + "\t" + item.IsHoliday);
Console.WriteLine("假期名稱: " + item.Name);
Console.WriteLine("假期說明: " + item.Description);
}
}
}
}
-
輸出結果如下
959 2021/1/1 是
假期名稱: 中華民國開國紀念日
假期說明: 全國各機關學校放假一日
960 2021/1/2 是
假期名稱:
假期說明:
961 2021/1/3 是
假期名稱:
假期說明:
重新調整資料模型類別
我們想把這兩個欄位定義成「非必要」的欄位,一般的時候不需要回應給用戶端知道,當前端需要的時候才需要序列化給用戶端。
事實上我們專案中的欄位有數十到數百個動態欄位,設計之初可以從需求訪談得知一些「必要」的欄位,並設計到強型別的模型類別中,但其他的擴充欄位,我們打算當成「額外的」附加資料,有資料的時候就回傳,沒資料的時候就完全看不到。
這時我打算調整一下我們的模型類別,將 Name
與 Description
欄位給刪除,並加入另一個特殊的 AdditionalData
屬性。
-
加入 AdditionalData
屬性並標示 [JsonExtensionData]
屬性(Attribute)
using System.Collections.Generic;
using Newtonsoft.Json;
namespace c1
{
public partial class Holiday
{
[JsonProperty("_id")]
public long Id { get; set; }
[JsonProperty("date")]
public string Date { get; set; }
// [JsonProperty("name")]
// public string Name { get; set; }
// [JsonProperty("description")]
// public string Description { get; set; }
[JsonProperty("holidayCategory")]
public string HolidayCategory { get; set; }
[JsonProperty("isHoliday")]
public string IsHoliday { get; set; }
[JsonExtensionData]
public IDictionary<string, object> AdditionalData { get; set; } = new Dictionary<string, object>();
}
}
這裡的 AdditionalData
屬性要標示 [JsonExtensionData]
屬性(Attribute),就必須要宣告型別為 IDictionary<string, object>
或 IDictionary<string, JToken>
才行!
-
然後將主程式修改成如下
using System;
using System.IO;
using Newtonsoft.Json;
namespace c1
{
class Program
{
static void Main(string[] args)
{
var data = JsonConvert.DeserializeObject<Holiday[]>(File.ReadAllText("./sample.json"));
foreach (var item in data)
{
Console.WriteLine(item.Id + "\t" + item.Date + "\t" + item.IsHoliday);
if (item.AdditionalData.Keys.Contains("name"))
{
Console.WriteLine("假期名稱: " + item.AdditionalData["name"]);
}
if (item.AdditionalData.Keys.Contains("description"))
{
Console.WriteLine("假期說明: " + item.AdditionalData["description"]);
}
}
}
}
}
簡單來說,這些所謂的「擴充欄位」全部都會自動放在 AdditionalData
這個 IDictionary<string, object>
字典型別的屬性下!
-
輸出結果如下
959 2021/1/1 是
假期名稱: 中華民國開國紀念日
假期說明: 全國各機關學校放假一日
960 2021/1/2 是
961 2021/1/3 是
注意:如果試圖存取一個不存在的 Key 將會導致 Unhandled exception. System.Collections.Generic.KeyNotFoundException: The given key 'description1' was not present in the dictionary.
例外發生!
序列化強型別物件到可彈性擴充屬性的 JSON 輸出
假設我們想將 Holiday
物件外加一些額外的屬性到序列化後的 JSON 資料中,就可以參考以下寫法:
-
主程式修改如下
using System;
using Newtonsoft.Json;
namespace c1
{
class Program
{
static void Main(string[] args)
{
var data = new Holiday()
{
Id = 959,
Date = "2021/1/1",
IsHoliday = "是"
};
Console.WriteLine(JsonConvert.SerializeObject(data));
data.AdditionalData.Add("name", "中華民國開國紀念日");
data.AdditionalData.Add("description", "全國各機關學校放假一日");
Console.WriteLine(JsonConvert.SerializeObject(data));
}
}
}
-
輸出結果如下
{"_id":959,"date":"2021/1/1","holidayCategory":null,"isHoliday":"是"}
{"_id":959,"date":"2021/1/1","holidayCategory":null,"isHoliday":"是","name":"中華民國開國紀念日","description":"全國各機關學校放假一日"}
合併多個欄位到另一個強型別的屬性中
本篇文章講解的「擴充屬性」技巧,也可以套用在 反序列化 (Deserialize) 的過程中,將兩個或多個屬性合併成一個或多個屬性,相當實用!
如下程式碼範例,你只要先定義出以下 DTO 模型物件,就可以做到更多反序列化過程的客製化行為:
- 定義一個
private void OnDeserialized(StreamingContext context)
方法,並套用 [OnDeserialized]
屬性(Attribute)
- 你可以從私有的
_additionalData
屬性取出 JSON 的鍵值,然後拆解到其他的強型別屬性中
public class DirectoryAccount
{
// normal deserialization
public string DisplayName { get; set; }
// these properties are set in OnDeserialized
public string UserName { get; set; }
public string Domain { get; set; }
[JsonExtensionData]
private IDictionary<string, JToken> _additionalData;
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
// SAMAccountName is not deserialized to any property
// and so it is added to the extension data dictionary
string samAccountName = (string)_additionalData["SAMAccountName"];
Domain = samAccountName.Split('\\')[0];
UserName = samAccountName.Split('\\')[1];
}
public DirectoryAccount()
{
_additionalData = new Dictionary<string, JToken>();
}
}
當然,如果你的擴充屬性是 type1
, type2
, type3
, ... 這類的資料,更可以利用這個技巧,將這些資料轉成強型別的陣列型別,讓你更有效率的操作資料!
相關連結