是的,JSON日期問題又來了!!

上回提過在Server端透過Reviver函式解析ISO 8601格式(yyyy-MM-ddTHH:mm:ssZ),但實務上Client端理Json.NET序列化字串時,還有一個小眉角: 時區問題。

Json.NET在進行日時轉換時有個參數--DateTimeZoneHandling,預設為RoundtripKind,故會保留時區資訊(Time zone information should be preserved when converting.)。而.NET的DateTime型別具有時區觀念,例如: DateTime.Now是本地時間,DateTime.UtcNow則是UTC時間,這兩種不同DateTimeKind經JsonConvert.SerializeObject()的結果不盡相同。

以下為簡單範例,透過JsonConvert分別序列化Now及UtcNow以比較差異,並示範透過JsonSerializerSettings指定DateTimeZoneHandling.Utc統一轉換為UTC時間,另外還展示透過DefaultSettings全域設定的小技巧。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
 
namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            //使用預設序列化參數
            Console.WriteLine("* Default JsonSerializerSettings");
            Console.WriteLine("DateTime.Now => {0}",
                JsonConvert.SerializeObject(DateTime.Now));
            Console.WriteLine("DateTime.UtcNow => {0}",
                JsonConvert.SerializeObject(DateTime.UtcNow));
 
            //序列化時傳入JsonSerializerSettings指定時區原則
            var js = new JsonSerializerSettings()
            {
                DateTimeZoneHandling = DateTimeZoneHandling.Utc
            };
            Console.WriteLine("* DateTimeZoneHandling.Utc");
            Console.WriteLine("DateTime.Now => {0}",
                JsonConvert.SerializeObject(DateTime.Now, js));
            Console.WriteLine("DateTime.UtcNow => {0}",
                JsonConvert.SerializeObject(DateTime.UtcNow, js));
            
            //可設成預設值
            Console.WriteLine("* Set DefaultSettings");
            JsonConvert.DefaultSettings = () => new JsonSerializerSettings()
            {
                DateTimeZoneHandling = DateTimeZoneHandling.Utc
            };
            Console.WriteLine("DateTime.Now => {0}",
                JsonConvert.SerializeObject(DateTime.Now));
            Console.WriteLine("DateTime.UtcNow => {0}",
                JsonConvert.SerializeObject(DateTime.UtcNow));
            Console.Read();
        }
    }
}

程式執行結果如下:

* Default JsonSerializerSettings
DateTime.Now => "2013-10-03T17:48:42.2826841+08:00"
DateTime.UtcNow => "2013-10-03T09:48:42.504104Z"
* DateTimeZoneHandling.Utc
DateTime.Now => "2013-10-03T09:48:42.5081116Z"
DateTime.UtcNow => "2013-10-03T09:48:42.5181306Z"
* Set DefaultSettings
DateTime.Now => "2013-10-03T09:48:42.5191325Z"
DateTime.UtcNow => "2013-10-03T09:48:42.5201344Z"

我們可以看見,依預設行為,DateTime.Now序列化時最後會加上+08:00時區資訊,而UtcNow則是以Z結尾。先前介紹的日期Reviver,比對格式為/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/,如要支援+08:00,勢必得修改RegExp樣式,並針對含時區資料時依本地時間方式處理。而另一個思考方向,若系統並不需要保留DateTime資料時區,讓傳回Client端的時間一律以UTC為準,再視需求調成Client端的本地時間,會是更簡單扼要的做法。當設定DateTimeZoneHandling.Utc,Json.NET便會忽略DateTime的時區資訊,一律轉為UTC標準時間。

這個問題在同樣採用Json.NET的ASP.NET WebAPI上也會發生,如以下WebAPI範例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
 
namespace MyWeb.Controllers
{
    public class DateTestController : ApiController
    {
        public class Result
        {
            public DateTime Now { get; set; }
            public DateTime UtcNow { get; set; }
            public Result()
            {
                Now = DateTime.Now;
                UtcNow = DateTime.UtcNow;
            }
        }
        [HttpPost]
        public Result Get() 
        {
            return new Result();
        }
    }
}

使用POSTMAN測試結果如下: (註: POSTMAN是所有Web API及AJAX程式開發者的好朋友,一個不可多得的Chrome外掛,不要多問,趕快下載安裝就對了!!)

如同先前的Console應用程式測試,Now的序列化結果也被加上+08:00時區資訊。若不調動ASP.NET MVC設定,我們可以對本地時間形式的DateTime做ToUniversalTime()自行轉為UTC時間。而斧底抽薪的做法,可以直接改變WebAPI的序列化設定,在App_Start/WebApiConfig.cs中加入config.Formatters.JsonFormatter.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc,要求所有DateTime在序列化都自動轉成UTC時間。

    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.Formatters.JsonFormatter.SerializerSettings.DateTimeZoneHandling
                = Newtonsoft.Json.DateTimeZoneHandling.Utc;
 
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
//...以下省略...

加上設定後,如下圖所示,不管Now或UtcNow,就都是"yyyy-MM-ddTHH:mm:ssZ"格式囉!

2017-08-08 補充更簡便的解法


Comments

# by jack

我使用了JavaScriptDateTimeConverter这个转换器,最后转换后的日期格式为2013-11-11T08-08-11 如何去掉这个日期中的T呢?

# by Jeffrey

to jack, 是在SerializeObject()將.NET DateTime轉為JSON時遇到這個問題嗎? 應是DateTime物件被設為DateTimeKind.Unspecified導致,試試用DateTime.ToUniversalTime()將日期轉為UTC時間。

# by jack

在web api中 所有的controller的返回值都为actionresult类型,我自定义了一个jsonresult类型继承自actionresult,里面用了json.net的JsonSerializer,converters用的是JavaScriptDateTimeConverter,前端用的是EXT,这样收到的时间格式为2013-11-11T08-08-11,中间带了个T,应该怎么处理?

# by jack

我是将所有的controller的返回值类型都特意弄成actionresult类型了,能否将所有的web api下的对外公开的服务提取一个统一的类型么?

# by Jeffrey

to jack, 2013-11-11T08-08-11是Json.NET轉換DateTimeKind.Unspecified(未指定時區類別)的DateTime所產生的ISO8601格式字串,你所謂的處理是指要將其轉成JavaScript端的Date型別嗎? 若是,可利用Reviver(參考: http://blog.darkthread.net/post-2013-05-11-json-parse-iso8601.aspx),因結尾沒有Z,預設Reviver邏輯無法轉換,你可以調整Reviver函式因應。但我習慣的解法是在Server端先將日期格式利用.ToUniversalTime()轉成UTC時間再交給Json.NET,如此轉出的格式會變成2013-11-11T08-08-11Z,就能順利在JavaScript用標準日期Reviver轉成Date了。

# by nightingale

我的想法比較簡單 仔細想想 為什麼會要用 Null 或 DateTime.MinValue 呢 因為通常都是拿來判斷,當User沒輸入日期時候! 要做什麼事情 所以直接在後面.Date就好了ar :P (tmpClass.SDate.Date == DateTime.MinValue.Date) ?

# by 曾永裕

typo: 故會包留時區資訊

# by Jeffrey

to 曾永裕,感謝指正~

# by Shih Hung

Dear 黑大: 目前專案上遇到時間長得像這樣2019/7/1 上午 08:49:21 在使用JsonConvert.DeserializeObject轉型時有特別在該欄位加上以下自行編寫的轉換[JsonConverter(typeof(ChineseDateTimeConvert))],但執行時還是顯示無法辨識的字串,不知道 是哪邊寫錯了,雖然可以先用文字在讀每一筆轉換,但是這樣程式執行太久... class ChineseDateTimeConvert : IsoDateTimeConverter { public ChineseDateTimeConvert() : base() { base.Culture = new CultureInfo("zh-TW"); base.DateTimeStyles = DateTimeStyles.None; base.DateTimeFormat = "yyyy/MM/dd tt hh:mm:ss"; } }

# by Jeffrey

to Shih Hung, 原來有這招。我猜你的問題是格式被寫成"yyyy/MM/dd...",應該要寫"yyyy/M/d..."才會匹配2019/7/1。另外,實測直接用 IsoDateTimeConverter 指定 Culture 跟 DateTimeFormate 就可以了,不需自訂類別,範例: var c = new IsoDateTimeConverter() { DateTimeFormat = "yyyy/M/d tt hh:mm:ss", Culture = new System.Globalization.CultureInfo("zh-tw") }; var obj = JsonConvert.DeserializeObject<MyClass>(json, c);

Post a comment