Home / AJAX

How do I Serialize Dates with JSON?

RSS
Modified on 2010/09/03 03:32 by Rick Strahl Categorized as Uncategorized

Problem

I am calling a Page Method or Web Service that returns a result that contains dates. The date values returned either as result values or child properties are showing up as encoded strings rather than dates. How can I get ‘real’ date values from my dates?

Solution

Date formatting with JSON is problematic as JavaScript doesn’t have a native Date literal. This means there’s no standard way to encode and parse dates in JavaScript. As a result current JSON parsers uses an informal string representation value to encode dates, but have no facility for de-serializing these date strings back into date values.

To get an idea how native date de-serialization works try running the following page:

Try Me!

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>JSON Date Parsing</title>
    <script src="Scripts/jquery-1.4.1.min.js" type="text/javascript"></script>
    <script src="Scripts/json2.min.js" type="text/javascript"></script>
</head>
<body>
    <div>
        <pre id="divMessage"></pre>

<script type="text/javascript"> function rawJSON() { var date = new Date(); var dateString = JSON.stringify(date); appendText("Date: " + date); appendText("Serialized: " + dateString);

var oldDate = JSON.parse(dateString); appendText("Deserialized: " + oldDate + "\r\n");

var obj = {} obj.name = "Rick"; obj.date = new Date(); var objString = JSON.stringify(obj); appendText("Date: " + obj.date) appendText("Serialized (object): " + objString);

var oldObj = JSON.parse(objString); appendText("Deserialized (date only): " + oldObj.date); } function appendText(text) { var jMsg = $("#divMessage") jMsg.text(jMsg.text() + "\r\n" + text); } </script>

</div> </body> </html>

The result from this is:

Date: Tue Aug 31 2010 13:40:12 GMT-1000 (Hawaiian Standard Time)Serialized: "2010-08-31T23:40:12.193Z" Deserialized: 2010-08-31T23:40:12.193Z Date: Tue Aug 31 2010 13:40:12 GMT-1000 (Hawaiian Standard Time) Serialized (object): {"name":"Rick","date":"2010-08-31T23:40:12.197Z"}Deserialized (date only): 2010-08-31T23:40:12.197Z

The first value in each of the sets is the original date followed by the serialized value (a simple date in the first example, an object containing a date in the second). You can see that date encoding occurs and the format used is what’s known as ISO encoding format:

"2010-08-31T01:35:05.785Z"

The json2.js parser and all the native browser JSON parsers create date values in this format with JSON.stringify(), however the JSON.parse() function does not parse that same value back into a date. Instead you just get ISO date string returned which is not terribly useful.

.NET JSON Date Serialization

Another complication when working with .NET is that ASP.NET AJAX and WCF Web Services generate dates in yet another format so the data returned from the server isn’t in the same format as ‘native’ JavaScript JSON dates. The date format returned from .NET services looks like this:

"\/Date(1283219926108)\/"

This format is a specially encoded string that minimizes conflicts, but is also a little tricky to parse and adds another check required to parse date strings.

The JavaScript de-serializer in .NET that is used for ASP.NET AJAX (ASMX and PageMethod callbacks) supports de-serialization of the client JSON generated ISO format. The WCF parser however only understands the .NET date syntax so if you plan on calling WCF services your client JSON encoding also has to encode dates into the Microsoft format.

Fixing up Date Parsing with Revivers and Replacers

JSON serialization is provided either by native browser support (for the latest versions of Internet Explorer, FireFox, Chrome, Opera and Safari) or by using json2.js by Douglas Crockford (json.org/json2.js for the latest version) on which the native browser JSON parsers are based. For more info on JSON parsing and json2.js check out the Page Methods topic.

The native JSON object in browsers and json2.js all support overriding of the parsing and encoding behavior by providing custom filters. The JSON objects stringify() and .parse() functions support parameters for Replacer and Reviver functions respectively. These functions allow hooking up custom parsing logic for each data value passed through it. Effectively these filter functions are run for each value/property of the objects parsed or encoded and allow inspection and modification of each value processed.

One approach to take advantage of this functionality is to extend the JSON object with additional functions that specifically support date parsing both for ISO and .NET style dates. Note that in order for this to work either a native JSON object or json2.js needs to be loaded prior to extending the JSON object.

The following code shows an example of JSON extensions (defined in ServiceProxy.js) that perform date parsing tasks useful for use with .NET Services and Page Methods:

if (this.JSON && !this.JSON.parseWithDate) {
    var reISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/;
    var reMsAjax = /^\/Date\((d|-|.*)\)[\/|\\]$/;

JSON.parseWithDate = function (json) { /// <summary> /// parses a JSON string and turns ISO or MSAJAX date strings /// into native JS date objects /// </summary> /// <param name="json" type="var">json with dates to parse</param> /// </param> /// <returns type="value, array or object" /> try { var res = JSON.parse(json, function (key, value) { if (typeof value === 'string') { var a = reISO.exec(value); if (a) return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6])); a = reMsAjax.exec(value); if (a) { var b = a[1].split(/[-+,.]/); return new Date(b[0] ? +b[0] : 0 - +b[1]); } } return value; }); return res; } catch (e) { // orignal error thrown has no error message so rethrow with message throw new Error("JSON content could not be parsed"); return null; } }; JSON.dateStringToDate = function (dtString) { /// <summary> /// Converts a JSON ISO or MSAJAX string into a date object /// </summary> /// <param name="" type="var">Date String</param> /// <returns type="date or null if invalid" /> var a = reISO.exec(dtString); if (a) return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6])); a = reMsAjax.exec(dtString); if (a) { var b = a[1].split(/[-,.]/); return new Date(+b[0]); } return null; }; JSON.stringifyWcf = function (json) { /// <summary> /// Wcf specific stringify that encodes dates in the /// a WCF compatible format ("/Date(9991231231)/") /// Note: this format works ONLY with WCF. /// ASMX can use ISO dates as of .NET 3.5 SP1 /// </summary> /// <param name="key" type="var">property name</param> /// <param name="value" type="var">value of the property</param> return JSON.stringify(json, function (key, value) { if (typeof value == "string") { var a = reISO.exec(value); if (a) { var val = '/Date(' + new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6])).getTime() + ')/'; this[key] = val; return val; } } return value; }) };

}

This code adds two parsing functions to the JSON object: .parseWithDate() and .dateStringToDate(). .parseWithDate() works like .parse() and parses the entire JSON object graph at once, while .dateToString() parses individual JSON date strings into data values. Both support parsing of both ISO and .NET style dates. While ‘global’ date parsing with .parseWithDate() may seem easier, keep in mind that date parsing for every element in a large object graph can be considerably slower than native JSON parsing as a regular expression is applied against each string property passed through the filter. If you really need each of the date values returned then using JSON.parseWithDate() is a good choice. If you only need to look at a few values then leaving date values as a string and individually calling JSON.dateStringToDate() as needed might perform much better.

The JSON.parseWithDate() and JSON.stringifyWcf() functions are drop in replacements for the native JSON.parse() and JSON.stringify() functions and should work identically. You can now do the following as before but get proper date fix-up:

Try Me!

function fixedupJSON() {
    var date = new Date();
    var dateString = JSON.stringifyWcf(date);
    appendText("Date: " + date);
    appendText("Serialized Date (.NET style): " + dateString);

var oldDate = JSON.parseWithDate(dateString); appendText("Deserialized: " + oldDate + "\r\n");

var obj = {} obj.name = "Rick"; obj.date = new Date(); var objString = JSON.stringify(obj); appendText("Date (Date Only): " + obj.date); appendText("Serialized (object):" + objString);

var oldObj = JSON.parseWithDate(objString); appendText("Deserialized (date only): " + oldObj.date); }

Note I’m using stringifyWcf here to demonstrate working with .NET style client dates, but you can also use the standard stringify method that encodes to ISO dates and that works as well. Both formats are supported for de-serialization by these functions.

Using a $.ajaxSetup( { dataFilter: } for Global Data Conversion

One additional step to automate date conversion generically for AJAX callbacks is to use the $.ajaxSetup() function and a dataFilter. A data filter in this scenario allows you explicitly process any AJAX callback result data, which effectively lets you override the default JSON parsing with custom parsing that take advantage of these JSON extension functions shown in the previous section. Once implemented functions like $.getJSON() and $.ajax() can automatically perform date transformations for every AJAX call.

The following dataFilter implementation (defined in ServiceProxy.js) demonstrates this functionality:

$.ajaxSetup({ dataFilter: function (jsonString, type) {
    if (type == "json") {        
        // Use json library so we can fix up dates        
        var res = JSON.parseWithDate(jsonString);
        if (typeof (res) === "string")
            return jsonstring; // return JSON as jQuery double parses        
        if (res && res.hasOwnProperty("d"))
            return (typeof res.d === "string") ?
                    JSON.stringify(res.d) : // jQuery double parses
                    res.d;
        return res;
    }
    return jsonString;
} });

With this filter and the JSON extensions in place you can now make a simple call to $.getJSON() and retrieve date values without further changes in code. The nice thing about this is that this will work with existing code without changes and lets you use jQuery the way it was intended but with custom behavior you control in one place.

For example, with the above setup in place you can call a custom handler that returns a date in JSON format:

public class DateHandler : IHttpHandler
{

public void ProcessRequest(HttpContext context) { context.Response.ContentType = "application/json"; string action = context.Request.QueryString["jsonmode"]; string json = null;

if (!string.IsNullOrEmpty(action) && action == ".net") { // Creates date in .NET date format "\/Date(14123123132)\/" JavaScriptSerializer ser = new JavaScriptSerializer(); json = ser.Serialize(DateTime.Now); } else // iso format: "2010-08-31T01:35:05.785Z" json = "\"" + DateTime.Now.ToUniversalTime().ToString("s") + "Z" + "\""; context.Response.Write(json); }

public bool IsReusable { get { return false; } } }

from the client with code like this:

Try Me!

function getDateFromHandler() {
      $.getJSON("DateHandler.ashx?mode=iso", {},
           function (date) { alert(date); }                          
      );
}

and get proper date translation from the $.getJSON() call. Along the same lines $.ajax() callbacks that use dataType: "json" also get the same transformations applied automatically. Using the ServiceProxy class calling an ASMX service that automatically transforms a date value looks like this:

Try Me!

function getDateFromService() {
    var curDate = new Date();
    var proxy = new ServiceProxy("PageMethodsService.asmx/");            
    proxy.invoke("GetTime", { time: curDate },
         function (time) { 
              alert("Current: " + curDate + "\r\nReturned: " + time); 	
         },
         function (error) { alert(error.message) });
}

Both examples return dates serialized by the server as 'real' JavaScript dates on the client.

Summary

JSON Date translation is tricky in JavaScript because JavaScript lacks an official date literal and the available JSON parsers do not implement JSON de-serialization of dates natively. This leaves you to implement a custom solution to date de-serialization. Luckily native JSON parsers and json2.js support extensibility of the parsing and encoding process that make it relatively easy to generically extend JSON parsing to support various date formats. Keep in mind though that these extensions come at a cost of JavaScript processing vs. native JSON parsing which can be extremely fast. Adding custom filters considerably slows performance of JSON parsing which might be an issue if you’re passing lots of data back from the server.

Another thing to keep in mind is that date de-serialization is workable on the client, but often you’ll find that native date support in JavaScript is minimal. For example, formatting dates to display in user friendly formats is not supported natively and you need to result to additional libraries or custom code to format dates properly for display. If your goal is to display dates on the client a better choice might be to return dates from the server pre-formatted as part of a custom service message/view that gets returned to the client. This is easy to do in .NET by using Projection in LINQ queries or by creating custom Views/Message result objects that are returned to the client.

About the Author

Rick Strahl is the big Kahuna at West Wind Technologies on Maui, Hawaii. The company specializes in Web and distributed application development, develops several commercial and free tools, provides training and mentoring with focus on .NET, IIS and Visual Studio. Rick's a C# Microsoft MVP, a frequent contributor to magazines and books, and a frequent speaker at developer conferences and user groups. He is also co-publisher of CoDe magazine. For more information please visit his blog at http://www.west-wind.com/weblog/ or contact Rick at rstrahl@west-wind.com.