Json.NET日期序列化的時區問題

是的,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 補充更簡便的解法

歡迎推文分享:
Published 03 October 2013 11:52 PM 由 Jeffrey
Filed under: ,
Views: 21,739



意見

# jack said on 11 November, 2013 12:55 AM

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

# Jeffrey said on 11 November, 2013 04:18 AM

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

# jack said on 15 November, 2013 12:43 AM

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

# jack said on 15 November, 2013 01:54 AM

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

# Jeffrey said on 16 November, 2013 07:54 AM

to jack, 2013-11-11T08-08-11是Json.NET轉換DateTimeKind.Unspecified(未指定時區類別)的DateTime所產生的ISO8601格式字串,你所謂的處理是指要將其轉成JavaScript端的Date型別嗎? 若是,可利用Reviver(參考: 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了。

# nightingale said on 30 December, 2014 09:59 PM

我的想法比較簡單

仔細想想

為什麼會要用 Null 或 DateTime.MinValue 呢

因為通常都是拿來判斷,當User沒輸入日期時候!

要做什麼事情

所以直接在後面.Date就好了ar :P

(tmpClass.SDate.Date == DateTime.MinValue.Date) ?

# 曾永裕 said on 02 August, 2016 04:25 AM

typo: 故會包留時區資訊

# Jeffrey said on 02 August, 2016 08:20 PM

to 曾永裕,感謝指正~

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<October 2013>
SunMonTueWedThuFriSat
293012345
6789101112
13141516171819
20212223242526
272829303112
3456789
 
RSS
創用 CC 授權條款
【廣告】
twMVC
最新回應

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication