JSON.NET 可以讓我們很方便地讀寫 JSON 字串並對應到 C# 類別。在大部份的時候我們都可以簡單地使用 SerializeObject() 和 DeserializeObject() 方法進行轉換。但是一旦遇到 JavaScript 日期, 這招可能就行不通了。因為有些 JavaScript 開發人員會使用一種特殊的 JavaScript 日期表示法 (不是 Date 型別, 而是 Int64 型別) 來代表日期...
JSON.NET 可以讓我們很方便地讀寫 JSON 字串並對應到 C# 類別。在大部份的時候我們都可以直接使用 SerializeObject() 和 DeserializeObject() 方法進行轉換。但是一旦遇到 JavaScript 日期, 這招可能就行不通了。因為有些 JavaScript 開發人員會使用一種特殊的 JavaScript 日期表示法 (不是 Date 型別, 而是 Int64 型別) 來代表日期。
若採用這種做法, 我們可能會在某個 JSON 檔案中看到以下這種資料格式:
{ "dt":1389843900000 }
這是什麼格式? 事實上, 如果你以後看到這種資料格式, 你應該就能判斷這就是一個日期, 但是其內容是以一個 Int64 型別的資料來表示。它實際上是從 1970/1/1 0:00:00 起算的毫秒數 (UTC 時間)。在 JavaScript 中, 我們可以使用 Date.UTC() 方法以取得這個值, 例如 Date.UTC(1970, 0, 1) 會得到 0。請注意這個 JavaScript 中最奇特的地方: 參數中月份是從 0 起算, 所以 0 是代表一月; 如果你輸入 12, 它會自動進位為隔年的一月。日期則是從 1 起算。同樣的, 如果你在日期欄位輸入 0, 它會視為前一天。這是 JavaScript 語言中最著名的陷阱之一, 要特別留意。若執行 Date.UTC(2014,3,9,23,59,59) (表示 2014/4/9 23:59:59) 則會得到 1397087999000。這裡的時間都是 UTC 時間, 所以我們使用時要再轉換到台灣時間 (UTC + 8 小時)。
我查了很久, 似乎這種表示式並沒有什麼特殊的名稱, 就是 "millisends" 而已。除了這種格式, 我們也常常見到另一種格式, 是以秒為單位的。所以如果你看到的日期只有十位數字, 那麼把它乘上 1000, 就等於本文中要轉換的時間單位了。
如果要在 JavaScript 中把這個數字轉回日期, 使用 new Date(1397087999000) 就可以傳回一個 Date 格式的物件, 而且它會自動幫你轉換成當地時間 (在此例中為 2014/4/10 7:59:59)。請注意一定要加上 new 關鍵字, 因為只有建構子會幫你做這個轉換。如果你忘了加上 new 字, 不管是 Date(1397087999000) 或者 Date(0) 都會默默地傳回現在時間, 不會出現任何錯誤訊息。
因此, 範例中的1389843900000 實際上就代表 2014/1/16 上午 11:45:00 (台灣時間)。知道其邏輯之後, 我們就可以在 C# 中簡單地寫個程式進行轉換。
但是, 我們要如何在 C# 類別中與 JSON 資料進行這種轉換呢? 如果你把這個欄位在 C# 類別中標示為 DateTime, 那麼一進行轉換, 就會出問題, 因為型別不對。我們有什麼辦法讓它自動進行轉換嗎?
例如, 假設你有一個 C# 類別如下:
// 程式一
[JsonObject(MemberSerialization.OptIn)]
public class EventTime
{
[JsonProperty("id")]
public int EventTimeId { get; set; }
[JsonProperty("time")]
public DateTime? Time { get; set; }
}
若照這種寫法, 程式根本無法執行。
我們必須自訂 Converter, 來做自動轉換的動作。為讀者方便起見, 我把我寫的轉換程式列在下面:
// 程式二
public class JsTimeConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.None) return null;
var time = (long)serializer.Deserialize(reader, typeof(long));
return ConvertJsTimeToNormalTime(time, true);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var item = (DateTime)value;
writer.WriteValue(ConvertToJsTime(item));
writer.Flush();
}
public long ConvertToJsTime(DateTime value)
{
return (long)value.ToUniversalTime()
.Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc))
.TotalMilliseconds;
}
public static DateTime ConvertJsTimeToNormalTime(long ticks, bool isTaiwanTime = true)
{
return new DateTime(1970, 1, 1).AddMilliseconds(ticks).AddHours(isTaiwanTime ? 8 : 0);
}
}
我們必須手動撰寫一個繼承 JsonConverter 的類別以進行轉換。其中的 WriteJson() 方法是輸出為 JSON 格式, ReadJson() 則是把 JSON 格式轉換為 C# 物件。把程式二加到你的程式裡, 然後再把程式一中的類別改成如下:
// 程式三
[JsonObject(MemberSerialization.OptIn)]
public class EventTime
{
[JsonProperty("id")]
public int EventTimeId { get; set; }
[JsonProperty("time")]
[JsonConverter(typeof(JsTimeConverter))]
public DateTime? Time { get; set; }
}
我們藉由加入 JsonConverter 這個 attribute, 指定針對該欄位 (Time) 的轉換程式。當我們使用 SerializeObject() 和 DeserializeObject() 方法讀寫 JSON 檔案時, 這轉換程式就會自動被呼叫, 做好無縫的格式轉換。
以下我把我的測試程式列出來:
// 程式四
static void Main(string[] args)
{
string json = "[ {\"id\": 1, \"time\":1389843900000}, {\"id\": 2, \"time\": 0} ]";
List<EventTime> list = testConverterIn(json);
Console.WriteLine(testConverterOut(list));
Console.WriteLine("Press any key to exit... ");
Console.ReadKey();
}
private static List<EventTime> testConverterIn(string input)
{
List<EventTime> list = new List<EventTime>();
List<EventTime> withTimes = null;
EventTimes = JsonConvert.DeserializeObject<List<EventTime>>(input);
foreach (EventTime wt in EventTimes)
{
Console.WriteLine("{0}. {1}", wt.EventTimeId, wt.Time);
list.Add(wt);
}
return list;
}
private static string testConverterOut(List<EventTime> input)
{
string output = JsonConvert.SerializeObject(input);
return output;
}
如果你有其它種類的轉換必要, 原則上就是如上所寫的, 先把繼承 JsonConverter 的工具類別寫好, 在裡面加上你自己的邏輯, 然後在對應的類別中, 在必須進行轉換的欄位上加上 [JsonConverter(typeof(你的轉換程式類別))] 即可。
以下是所有桯式的列表:
// 程式五
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace JsonTest
{
class Program
{
static void Main(string[] args)
{
string json = "[ {\"id\": 1, \"time\":1389843900000}, {\"id\": 2, \"time\": 0} ]";
List<EventTime> list = testConverterIn(json);
Console.WriteLine(testConverterOut(list));
Console.WriteLine("Press any key to exit... ");
Console.ReadKey();
}
private static List<EventTime> testConverterIn(string input)
{
List<EventTime> list = new List<EventTime>();
List<EventTime> EventTimes = null;
EventTimes = JsonConvert.DeserializeObject<List<EventTime>>(input);
foreach (EventTime wt in EventTimes)
{
Console.WriteLine("{0}. {1}", wt.EventTimeId, wt.Time);
list.Add(wt);
}
return list;
}
private static string testConverterOut(List<EventTime> input)
{
string output = JsonConvert.SerializeObject(input);
return output;
}
}
public class JsTimeConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.None) return null;
var time = (long)serializer.Deserialize(reader, typeof(long));
return ConvertJsTimeToNormalTime(time, true);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var item = (DateTime)value;
writer.WriteValue(ConvertToJsTime(item));
writer.Flush();
}
public long ConvertToJsTime(DateTime value)
{
return (long)value.ToUniversalTime()
.Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc))
.TotalMilliseconds;
}
public static DateTime ConvertJsTimeToNormalTime(long ticks, bool isTaiwanTime = true)
{
return new DateTime(1970, 1, 1).AddMilliseconds(ticks).AddHours(isTaiwanTime ? 8 : 0);
}
}
[JsonObject(MemberSerialization.OptIn)]
public class EventTime
{
[JsonProperty("id")]
public int EventTimeId { get; set; }
[JsonProperty("time")]
[JsonConverter(typeof(JsTimeConverter))]
public DateTime? Time { get; set; }
}
}