97

I have a date object that's created by the user, with the timezone filled in by the browser, like so:

var date = new Date(2011, 05, 07, 04, 0, 0);
> Tue Jun 07 2011 04:00:00 GMT+1000 (E. Australia Standard Time)

When I stringify it, though, the timezone goes bye-bye

JSON.stringify(date);
> "2011-06-06T18:00:00.000Z"

The best way I can get a ISO8601 string while preserving the browser's timezone is by using moment.js and using moment.format(), but of course that won't work if I'm serializing a whole command via something that uses JSON.stringify internally (in this case, AngularJS)

var command = { time: date, contents: 'foo' };
$http.post('/Notes/Add', command);

For completeness, my domain does need both the local time and the offset.

XwipeoutX
  • 4,765
  • 4
  • 29
  • 41

7 Answers7

105

Assuming you have some kind of object that contains a Date:

var o = { d : new Date() };

You can override the toJSON function of the Date prototype. Here I use moment.js to create a moment object from the date, then use moment's format function without parameters, which emits the ISO8601 extended format including the offset.

Date.prototype.toJSON = function(){ return moment(this).format(); }

Now when you serialize the object, it will use the date format you asked for:

var json = JSON.stringify(o);  //  '{"d":"2015-06-28T13:51:13-07:00"}'

Of course, that will affect all Date objects. If you want to change the behavior of only the specific date object, you can override just that particular object's toJSON function, like this:

o.d.toJSON = function(){ return moment(this).format(); }
Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • 1
    Thanks for the hint about overriding the toJSON method, that indeed solved my problem. but is there any safe way of doing it? I mean if I override it, It will be overridden everywhere, and it will mess .stringify elsewhere if I were using it. – Kamyar Ghasemlou Aug 29 '15 at 14:47
  • 4
    @KamyarGhasemlou - Updated the question to show one way. You can change a single `Date` object instance's `toJSON` function instead. – Matt Johnson-Pint Aug 29 '15 at 22:48
  • 2
    Thanks for the reply and quick addition. I should have thought about overriding method for a specific object, very clever. :) – Kamyar Ghasemlou Aug 29 '15 at 22:56
  • Saved a good night sleep :) I was stuck on this for quite some while. Just import and override it in the module.js (angular 2+) – Nenad Vichentikj Mar 03 '20 at 21:22
56

I'd always be inclined to not mess with functions in the prototype of system objects like the date, you never know when that's going to bite you in some unexpected way later on in your code.

Instead, the JSON.stringify method accepts a "replacer" function (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter) which you can supply, allowing you to override the innards of how JSON.stringify performs its "stringification"; so you could do something like this;

var replacer = function(key, value) {

   if (this[key] instanceof Date) {
      return this[key].toUTCString();
   }
   
   return value;
}

console.log(JSON.stringify(new Date(), replacer));
console.log(JSON.stringify({ myProperty: new Date()}, replacer));
console.log(JSON.stringify({ myProperty: new Date(), notADate: "I'm really not", trueOrFalse: true}, replacer));
Shawson
  • 1,858
  • 2
  • 24
  • 38
  • 4
    I just realized that I cannot mess with `value` as well. Have to use `this[key]`. – Polv Feb 11 '20 at 16:27
  • @Polv yea it's not the most intuitive! – Shawson May 21 '21 at 08:27
  • 11
    To save others the same struggle: replacer is called recursively, where `this` is the current node of the object, `key` the property being converted, and `value` a pre-transformed property value, being a string when original was a Date. Hence: `this[key]`, which gives you a Date rather than `value`, which gives you a string. – Grimace of Despair Jul 25 '21 at 05:27
  • 1
    Brilliant, thanks so much for this (no pun intended). It's worth it pointing out though that `this[key]` won't work in arrow functions. – Fappaz Sep 12 '21 at 02:58
  • 1
    @GrimaceofDespair I just wasted so much time on this! Do you have any idea why `this[key]` and `value` are *not* the same thing? I couldn't find any documentation about it. – Papooch Oct 12 '22 at 14:21
36

Based on Matt Johnsons 's answer, I re-implemented toJSON without having to depend on moment (which I think is a splendid library, but a dependency in such a low level method like toJSON bothers me).

Date.prototype.toJSON = function () {
  var timezoneOffsetInHours = -(this.getTimezoneOffset() / 60); //UTC minus local time
  var sign = timezoneOffsetInHours >= 0 ? '+' : '-';
  var leadingZero = (Math.abs(timezoneOffsetInHours) < 10) ? '0' : '';

  //It's a bit unfortunate that we need to construct a new Date instance 
  //(we don't want _this_ Date instance to be modified)
  var correctedDate = new Date(this.getFullYear(), this.getMonth(), 
      this.getDate(), this.getHours(), this.getMinutes(), this.getSeconds(), 
      this.getMilliseconds());
  correctedDate.setHours(this.getHours() + timezoneOffsetInHours);
  var iso = correctedDate.toISOString().replace('Z', '');

  return iso + sign + leadingZero + Math.abs(timezoneOffsetInHours).toString() + ':00';
}

The setHours method will adjust other parts of the date object when the provided value would "overflow". From MDN:

If a parameter you specify is outside of the expected range, setHours() attempts to update the date information in the Date object accordingly. For example, if you use 100 for secondsValue, the minutes will be incremented by 1 (minutesValue + 1), and 40 will be used for seconds.

bvgheluwe
  • 853
  • 7
  • 25
  • 1
    Perhaps it would be better to allow for minutes in the timezone offset. ie. `var offsetMinutes = this.getTimezoneOffset() % 60` – Jonathan Wilson Sep 17 '16 at 18:48
  • I think I would use a solution like this if I didn't already have moment js as a dependency – Jonathan Wilson Sep 17 '16 at 18:49
  • @JonathanWilson: I'm unsure why you would need the remaining minutes. Afaik, a timezone offset is always a whole number of hours. – bvgheluwe Sep 20 '16 at 12:34
  • 5
    you are correct that it usually is whole hours but I know for a fact that there are timezone offsets with minutes too. Nepal, for example is +05:45. I think it's a good idea when posting a general solution about time and timezones, to make it as robust as possible. – Jonathan Wilson Sep 20 '16 at 14:22
  • 1
    @Endless, as per your demands, I removed `: string` (three places) and `: Date` (one place). Now it's plain javascript. – bvgheluwe Nov 29 '16 at 13:48
  • 1
    Further to @JonathanWilson: Australia has non-hour based timezones as well – statler Jul 20 '18 at 07:08
12

When I stringify it, though, the timezone goes bye-bye

That’s because Tue Jun 07 2011 04:00:00 GMT+1000 (E. Australia Standard Time) is actually the result of the toString method of the Date object, whereas stringify seems to call the toISOString method instead.

So if the toString format is what you want, then simply stringify that:

JSON.stringify(date.toString());

Or, since you want to stringify your “command” later on, put that value in there in the first place:

var command = { time: date.toString(), contents: 'foo' };
CBroe
  • 91,630
  • 14
  • 92
  • 150
  • I get that I can just do it myself, but I'm after a way of doing it with an existing object. It's pretty dodgy having to translate the whole object when I just want to specify how one part serializes. – XwipeoutX Jun 28 '15 at 11:32
  • 1
    You can `date.toJSON = function () { return this.toString(); }` to specify how you want to stringify just this 'date' of your whole object. – James Jun 28 '15 at 12:22
  • `toString` might have the offset, but it's not in ISO format. – Matt Johnson-Pint Jun 28 '15 at 20:50
2

I've created a small library that preserves the timezone with ISO8601 string after JSON.stringify. The library lets you easily alter the behavior of the native Date.prototype.toJSON method.

npm: https://www.npmjs.com/package/lbdate

Example:

lbDate().init();

const myObj = {
  date: new Date(),
};

const myStringObj = JSON.stringify(myObj);

console.log(myStringObj);

// {"date":"2020-04-01T03:00:00.000+03:00"}

The library also gives you options to customize the serialization result if necessary.

1

If you have a JS Date Object and want to stringify it to preserve the timezone, then you should definitely use toLocaleDateString(). It is a very powerful helper function that can help you format your Date object in every way possible.

For example, if you want to print "Friday, February 1, 2019, Pacific Standard Time",

 const formatDate = (dateObject : Date) => {
    const options: any  = {
        weekday: 'long',
        year: 'numeric',
        month: 'long',
        day: 'numeric',
        timeZoneName: 'long'
      };
      
    return dateObject.toLocaleDateString('en-CA', options);
  };

Thus, by modifying the options object, you can achieve different styles of formatting for your Date Object.

For more information regarding the ways of formatting, refer to this Medium article: https://medium.com/swlh/use-tolocaledatestring-to-format-javascript-dates-2959108ea020

Avik
  • 371
  • 4
  • 15
0

let date = new Date(JSON.parse(JSON.stringify(new Date(2011, 05, 07, 04, 0, 0))));

Sujith S
  • 555
  • 5
  • 8