読者です 読者をやめる 読者になる 読者になる

ぼろぼろ平原

困った

RubyでTimeオブジェクトを"正しい"JSONに出力する

TL;DR: JSONは日時の表現方法を定めていないので好きにして良い。ただし、JavaScriptYAMLは定めているので迷ったらそれに従う。

JSONの日時の表記

JSONでは以下のデータ型しか扱えないので、日時は表現できない。

  • string
  • number
  • object
  • array
  • true
  • false
  • null

名前が紛らわしいが、「object」というのはRubyでいうHashに相当するもの(keyとvalueのペア)なので、RubyのObjectとは別物。

RubyJSONライブラリでの日時型の扱い

Rubyには複数のJSONライブラリがある。

それらで日時t = Time.parse("2016-02-21T06:11:12.110028+09:00")JSONに変換した時の結果は以下のようになる。 *1

参考までに、YAML.dumpTime#iso8601などの結果も載せている。

表記 結果
JSON.dump(t) "\"2016-02-21 06:11:12 +0900\""
Oj.dump(t) "{\"^t\":1456002672.110028000e32400}"
Oj.dump(t, mode: :compat) "1456002672.110028000e32400"
Oj.dump(t, mode: :compat, time_format: :unix) "1456002672.110028000"
Oj.dump(t, mode: :compat, time_format: :xmlschema) "\"2016-02-21T06:11:12.110028000+09:00\""
Oj.dump(t, mode: :compat, time_format: :ruby) "\"2016-02-21 06:11:12 +0900\""
t.to_s "2016-02-21 06:11:12 +0900"
t.iso8601 "2016-02-21T06:11:12+09:00"
t.iso8601(6) "2016-02-21T06:11:12.110028+09:00"

ライブラリやオプションの違いによって結果がバラバラになる。

JSONを復号した時に正しくTimeクラスになるのはOj.dump(t)の時だけで、JSON.dump(t)は復号するとStringクラス、Oj.dump(t, mode: :compat)BigDecimalクラスになる。

RubyYAMLライブラリでの日時の扱い

JSONとは異なり、YAMLでは日時の表記を定めている *2

基本的にはISO8601のサブセットで、スペースの有無、大文字小文字、小数点以下の桁数などは任意。

日時t = Time.parse("2016-02-21T06:11:12.110028+09:00")YAMLに変換した時の結果を示す。

表記 結果
YAML.dump(t) "2016-02-21 06:11:12.110028000 +09:00"

日時部分だけ抜粋した。

仕様にはスペース無し、Tは大文字、タイムゾーンUTCが正式(canonical)だと記載されているが、RubyYAMLライブラリは違う形式を出力する。

JavaScriptのtoJSON()での日時の扱い

JavaScriptでは、Date.prototype.toJSON()Date.prototype.toISOString()を使って日時をJSONに変換する。

toISOString()は常に24文字長のYYYY-MM-DDTHH:mm:ss.sssZという形式になる。

タイムゾーンは、接尾辞Zで表記されているように、常にUTCオフセットになる。

日時var d = new Date("2016-02-21T06:11:12.110028+09:00")JSONに変換した時の結果を示す。

表記 結果
d.toJSON() "2016-02-20T21:11:12.110Z"

結論

1. 復号した時に元のTimeクラスに戻ってほしいとき

require "oj"

Oj.dump(t) # => "{\"^t\":1456002672.110028000e32400}"
Oj.load(Oj.dump(t)).class # => Time

2. JavaScriptJSONとの互換性が必要な時

秒の小数第4位以下とタイムゾーンの情報は失われる。

require "oj"

class Time
  def to_json
    '"' + utc.iso8601(3) + '"'
  end
end

Oj.dump(t, mode: :compat) #=> "\"2016-02-20T21:11:12.110Z\""

3. YAMLとの互換性が必要な時

require "oj"

class Time
  def to_json
    '"' +  iso8601(9) "'"
  end
end

Oj.dump(t, mode: :compat) #=> "2016-02-21T06:11:12.110028000+09:00"

参考文献

*1:JSONライブラリをrequrieするとTime#to_jsonが上書きされるのでOj.dumpの結果がこの表と変わる

*2:working draft