Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DateTime serialization and deserialization

I'd like to serialize a Ruby DateTime object to json. Unfortunately, my approach is not symetrical:

require 'date'
date = DateTime.now
DateTime.parse(date.to_s) == date
 => false

I could use some arbitrary strftime/parse string combination, but I believe there must be a better approach.

like image 860
jupp0r Avatar asked Dec 08 '22 19:12

jupp0r


1 Answers

The accepted answer is not a good solution, unfortunately. As always, marshal/unmarshal is a tool you should only use as a last resort, but in this case it will probably break your app.

OP specifically mentioned serializing a date to JSON. Per RFC 7159:

JSON text SHALL be encoded in UTF-8, UTF-16, or UTF-32. The default encoding is UTF-8, and JSON texts that are encoded in UTF-8 are interoperable in the sense that they will be read successfully by the maximum number of implementations; there are many implementations that cannot successfully read texts in other encodings (such as UTF-16 and UTF-32).

Now let's look at what we get from Marshal:

marsh = Marshal.dump(DateTime.now)
# => "\x04\bU:\rDateTime[\vi\x00i\x03\xE0\x7F%i\x02s\xC9i\x04\xF8z\xF1\"i\xFE\xB0\xB9f\f2299161"
puts marsh.encoding
# -> #<Encoding:ASCII-8BIT>

marsh.encode(Encoding::UTF_8)
# -> Encoding::UndefinedConversionError: "\xE0" from ASCII-8BIT to UTF-8

In addition to returning a value that isn't human-readable, Marshal.dump gives us a value that can't be converted to UTF-8. That means the only way to put it into (valid) JSON is to encode it somehow, e.g. base-64.

There's no need to do that. There's already a very interoperable way to represent dates and times: ISO 8601. I won't go over why it's the best choice for JSON (and in general), but the answers here cover it well: What is the "right" JSON date format?.

Since Ruby 1.9.3 the DateTime class has had iso8601 class and instance methods to parse and format ISO 8601 dates, respectively. The latter takes an argument to specify precision for fractional seconds (e.g. 3 for milliseconds):

require "date"

date = DateTime.now
str = date.iso8601(9)
puts str
# -> 2016-06-28T09:35:58.311527000-05:00

DateTime.iso8601(str) == date
# => true

Note that if you specify a smaller precision, this might not work, because e.g. 58.311 is not equal to 58.311527. A precision of 9 (nanosecond) seems safe to me, since the DateTime docs say:

The fractional number’s precision is assumed at most nanosecond.

However, if you're interoperating with systems that might use greater precision, you should take that into consideration.

Finally, if you want to make Ruby's JSON library automatically use iso8601 for serialization, override the as_json and to_json methods:

unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
  require 'json'
end
require 'date'

class DateTime
  def as_json(*)
    iso8601(9)
  end

  def to_json(*args)
    as_json.to_json(*args)
  end
end

puts DateTime.now.to_json
# -> "2016-06-28T09:35:58.311527000-05:00"
like image 175
Jordan Running Avatar answered Dec 21 '22 22:12

Jordan Running