Maintaining JavaScript Date Values During Deserialization With A JSON Reviver
I'm currently making my way through Exploring ES6: Upgrade to the Next Version of JavaScript by Dr. Axel Rauschmayer. While it's a seemly-exhaustive in-depth look at ES6, it's also teaching me a few things about ES5. In particular, I learned that the JSON.parse() and JSON.stringify() methods accept additional helper functions that can influence the way the data is transformed during the serialization life-cycle. This got me thinking about the JavaScript Date object. Traditionally, the Date object doesn't survive deserialization; but, it seems like we could use one of these JSON helper functions to parse Date strings during deserialization.
First, let's take a quick look at the natural behavior of Date objects in the serialization-deserialization life-cycle. In the following demo, we're going to create an object that has embedded Date objects. Then, we're going to serialize it as JSON (JavaScript Object Notation) and immediately parse it back into a JavaScript object:
// Require the core node modules.
var chalk = require( "chalk" );
// Let's create a data structure that has embedded JavaScript Date objects. This could
// be a data structure that we created by hand; or, as another example, it could be a
// record-set that we received from the database (in which the DATETIME type was parsed
// into actual Date objects).
var friends = [
{
id: 1,
name: "Kim",
birthday: new Date( "1974 02 04 10:34:00" )
},
{
id: 2,
name: "Tricia",
birthday: new Date( "1981 12 19 23:00:14" )
}
];
// Now, let's serialize this data structure and then deserialize it so we can see how
// embedded Date objects are handled by default.
var serialized = JSON.stringify( friends );
var parsed = JSON.parse( serialized );
// Log the before and after structures.
console.log( chalk.magenta( "The Natural JSON Serialization Life-Cycle" ) );
console.log( chalk.cyan( "Original Data Structure" ) );
console.log( friends );
console.log( chalk.cyan( "Serialized -> Deserialized Data Structure" ) );
console.log( parsed );
When we run this code, we get the following terminal output:
As you can see, the embedded Date objects come out as Strings after being serialized and deserialized.
Now, let's see how we can reanimate the Date object by using a "reviver" function. The reviver is a mapping function that's responsible for mapping every single value into the deserialized data structure. Not only does this have the power to transform a given value, it has the ability to remove that value from the result entirely (by returning Undefined or, failing to return anything).
That sounds more complicated than it is. We don't actually have to map every value explicitly; if we don't want to transform a given value, we just pass-it through, as is, to the result. In our case, we only want to target String values that represent serialized Date objects. And, in that specific case, we want to return those string values as parsed Date objects.
// Require the core node modules.
var chalk = require( "chalk" );
// Let's create a data structure that has embedded JavaScript Date objects. This could
// be a data structure that we created by hand; or, as another example, it could be a
// record-set that we received from the database (in which the DATETIME type was parsed
// into actual Date objects).
var friends = [
{
id: 1,
name: "Kim",
birthday: new Date( "1974 02 04 10:34:00" )
},
{
id: 2,
name: "Tricia",
birthday: new Date( "1981 12 19 23:00:14" )
}
];
// Now, let's serialize this data structure and then deserialize it so we can see how
// embedded Date objects are handled by default.
var serialized = JSON.stringify( friends );
// This time, when we parse the JSON, we're going to pass-in a "reviver". This is a
// mapping function that is responsible for mapping every single value in the
// deserialized object. We can use this to inspect and parse serialized Date strings
// back into Date objects.
var parsed = JSON.parse( serialized, dateReviver );
// Log the before and after structures.
console.log( chalk.magenta( "The JSON + Reviver Serialization Life-Cycle" ) );
console.log( chalk.cyan( "Original Data Structure" ) );
console.log( friends );
console.log( chalk.cyan( "Serialized -> Deserialized Data Structure" ) );
console.log( parsed );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// I am a JSON.parse() reviver that will parse serialized Date objects back into actual
// Date objects.
// --
// CAUTION: This gets called for every single value in the deserialized structure.
function dateReviver( key, value ) {
if ( isSerializedDate( value ) ) {
return( new Date( value ) );
}
// If it's not a date-string, we want to return the value as-is. If we fail to return
// a value, it will be omitted from the resultant data structure.
return( value );
}
// I determine if the given value is a string that matches the serialized-date pattern.
function isSerializedDate( value ) {
// Dates are serialized in TZ format, example: '1981-12-20T04:00:14.000Z'.
var datePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
return( isString( value ) && datePattern.test( value ) );
}
// I determine if the given value is a String.
function isString( value ) {
return( {}.toString.call( value ) === "[object String]" );
}
As you can see, the dateReviver() function targets serialized Date strings and ignores everything else, so to speak. When it encounters a serialized Date string, it simply passes it through the Date constructor when returning it. And, when we run this code, we get the following terminal output:
Here, we can see that the serialized Date strings were parsed back into true JavaScript Date objects by the reviver. And, as a result, the input and output data structures are exactly the same.
This is really cool! I can't believe I've been using the JavaScript's JSON module for years and had no idea this feature was there. Granted, it's not a feature that I'll need all that often; but, having finer-grained control over the serialization and deserialization process is most certainly something I could use from time to time.
Want to use code from this post? Check out the license.
Reader Comments
Okay that's pretty awesome. I had the same " you mean to say...?!" moment. Why didn't they just make the deserializer handle dates properly if it had this ability already??
@Kristopher,
Ha ha, I know what you mean. On a somewhat related note, however, I just ran into a problem relating to JSON and dates. I have a migration system that takes data out of MySQL in one database, writes it to disk as JSON, and then reads it in and pushes it to another MySQL database:
MySQL --> JSON + JSON --> MySQL
The MySQL driver I was using (in Node) would convert the MySQL dates to JavaScript dates. Then, the JSON serialization would lead to the TZ format in this post. Then, when I went to read those values in a JSON and write them to a new database, the target database was complaining about the date format :D
I ended up having to tell the MySQL driver *not* to convert the MySQL dates to JavaScript dates; but, the moral of the store is, dates and serialization are hard :D
*mind blown*
we get so use to using various functions and feel like we know them so well, that we forget to refer to the spec. This is great stuff. Thanks!
@Chris,
Yooo, for real. I've been using the JSON object for *years* and I had no idea this was even in there :D