How to serialize Date and DateTime to JSON without losing information

When building APIs, it is pretty common to use JSON as a serialization format. JSON defines serialization for boolean, number and string, but not for date/datetime values.

What most serializers do with Date and DateTime values is to use the ISO8601 standard. For example:

# Date format
2011-07-14

# DateTime format
2011-07-14T19:43:37+0100

However, you should be aware that information is lost when you use the Date format. That happens because a Date value might differ between different timezones. Let me give you an example:

  • a request was issued at 2011-07-14 T01:00:00Z (UTC) from Brazil (UTC-0300) to a service in UTC
  • if the time the request was created is exposed as a Date, it would return the value as 2011-07-14
  • but from the client’s perspective in Brazil, the correct date is 2011-07-13, since at the moment of that request was issued, the local time in Brazil was 2011-07-13 T22:00:00-0300

If this information is used only inside your app, within the same timezone, you would have no problems. But, if you need to make this information available through a public API, one of your API’s consumers might recover an incorrect value.

So, from this experience, any date value that will be shared and consumed by different clients should be represented as date time with explicit timezone or using the Unix time format. That way, it is up to the client to treat the data properly.

Here is an example of an API that returns a subscription period in the right way:

{
  period_start_date: 1409175049, # Unix time
  period_end_date: 2014-09-27T18:30:49-0300 # ISO8601
}

# Time.at(1409175049)
# DateTime.parse(“2014-09-27T18:30:49-0300”)

The options above have the advantage that both are unambiguous and sortable. You may choose between one or another based on the fact that the timezone option is easier for human comprehension. But remember that Timezones are a presentation-layer problem, so if you just need to pass data around, Unix time is preferable.

Have you ever had this serialization problem or any other that caused information to be lost? If you have questions or any experience to share, please, leave a comment below.

Subscribe to our blog
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someone
  • Matt Melton

    Unfortunately this is an infuriating problem.

    While the offset in minutes (-0300) might accurately reflect Brazil, you’ve actually lost the relevant time zone id of the country.

    For example, try resolving the local time taking into account the DST (daylight savings time), ie: can you accurately print 10am HAST or HADT? What about countries that share the same longitude but use different time suffixes for their language locale?

    While I’ll be the first to admit that Date/DateTime don’t actually encode a geographic location (whether it’s a Windows TimeZone Id or IANA/Olson TZDB key), I wish it did.

  • Matt Jones

    I’m not following why the choice is between Unix time or an ISO8601-with-timezone. The Unix time is seconds since 1970-01-01T00:00:00Z, so it seems like you’ve got the same “was it actually the 13th or the 14th” difficulty.

    Conversely, if Unix time is OK then what’s wrong with ISO8601-normalized-to-UTC?

  • Unix time considers positive integers since 1970, but negative ones represents prior to this date. There is not problem in using ISO8601, it is just a matter of preference. As I pointed, ISO8601 is human readable so it is easy to reason about the date, if debugging for example.

    However, in general this info is consumed programmatically, so it doesn’t need to be readable, it just need to correct.

  • Yeah, dealing with date is not that simple. As you mentioned, geolocation is not encoded in Date/DateTime format, I see it as an extra data required to properly resolve the date value. For example, in Rails in order to properly parse a date time value on the desired timezone I could use:

    Time.use_zone(“Hawaii”) { Time.zone.parse(str) }
    # or
    Time.use_zone(“Hawaii”) { Time.zone.at(timestamp) }

    The idea of using Unix time or ISO8601 is to provide data that can be properly recovered no matter the consumer timezone, because it is not my concern.

    Not sure if I answered your question.