Rails Rails 里的时间 to_json 返回的格式问题

jzlikewei · August 25, 2017 · Last by rainchen replied at August 28, 2017 · 3646 hits

发现 ActiveRecord 的 model 里面的时间戳在调用 as_json 返回的格式的有点不同

2.4.1 :011 > PlayRecord.last.created_at.to_json
 => "\"2017-08-25T21:30:28.388Z\""
2.4.1 :012 > PlayRecord.last.created_at.as_json
 => "2017-08-25T21:30:28.388Z"
2.4.1 :013 > PlayRecord.last.created_at.to_s
 => "2017-08-25 21:30:28 UTC"
2.4.1 :014 > PlayRecord.last.created_at.class
 => Time
2.4.1 :015 > Time.now.to_s
 => "2017-08-25 21:39:50 +0800"
2.4.1 :016 > Time.now.to_json
 => "\"2017-08-25T21:39:56.335+08:00\""

简单来讲,就是 ActiveRecord 的 model 在 to_json 返回的时间格式是"2017-08-25T21:30:28.388Z",

而调用 to_s 返回的时间格式是"2017-08-25 21:30:28 UTC"

而 Time 对象调用 to_s 和 to_json 的结果是"2017-08-25 21:39:50 +0800"和 "\"2017-08-25T21:39:56.335+08:00\""

我看了下 activesupport 中关于 time 的 源码 看起来也没有改动 to_s 的默认行为啊

def to_formatted_s(format = :default)
    if formatter = DATE_FORMATS[format]
      formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
    else
      to_default_s
    end
  end
  alias_method :to_default_s, :to_s
  alias_method :to_s, :to_formatted_s

刚刚好这里有篇群里转的文章,分享给你 http://blog.lanvige.com/tech/datetime/

简单的说,to_json 中 DateTime 使用的是 iSO 8601 标准格式,而 to_s 的源码我这里版本是这样,也就是 time.strftime 直接格式化输出

def to_s(format = :default)
  if format == :db
    utc.to_s(format)
  elsif formatter = ::Time::DATE_FORMATS[format]
    formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
  else
    "#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby Time#to_s format
  end
end
alias_method :to_formatted_s, :to_s

你可以自己查看方法定义

PlayRecord.last.created_at.method(:to_s).source_location
Reply to lgn21st

那篇文章我通过 google 搜到过,看过了,并不能解决我的疑问

2.4.1 :005 > PlayRecord.last.created_at.method(:to_s).source_location
 => ["/Users/ilike/.rvm/gems/ruby-2.4.1/gems/activesupport-5.0.3/lib/active_support/core_ext/time/conversions.rb", 49] 
2.4.1 :008 >   Time.now.method(:to_s).source_location
 => ["/Users/ilike/.rvm/gems/ruby-2.4.1/gems/activesupport-5.0.3/lib/active_support/core_ext/time/conversions.rb", 49] 
2.4.1 :009 > Time.now.to_s
 => "2017-08-25 22:37:01 +0800" 
2.4.1 :010 > PlayRecord.last.created_at.to_s
 => "2017-08-25 13:13:28 UTC" 
2.4.1 :011 > PlayRecord.last.created_at.class==Time.now.class
 => true 


源代码里的这部分是

def to_formatted_s(format = :default)
   if formatter = DATE_FORMATS[format]
     formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
   else
     to_default_s
   end
 end
 alias_method :to_default_s, :to_s
 alias_method :to_s, :to_formatted_s

两个一样的类,同一个函数,返回的结果是不一样的,这个让我有点迷惑

Reply to jzlikewei

我们的 Rails 版本不一样,你这里需要去看一下 to_default_s 的方法实现。

Reply to lgn21st

彻底解决了

/active_support/core_ext/object/json.rb 这个文件给 Time 类加了 as_json 的函数:

class Time
  def as_json(options = nil) #:nodoc:
    if ActiveSupport::JSON::Encoding.use_standard_json_time_format
      xmlschema(ActiveSupport::JSON::Encoding.time_precision)
    else
      %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
    end
  end
end

具体在 Time 的 xmlschema 函数里会根据是否是 utc 时间来给出不同的格式,这里会直接 format,并不会调用 to_s

def xmlschema(fraction_digits=0)
    fraction_digits = fraction_digits.to_i
    s = strftime("%FT%T")
    if fraction_digits > 0
      s << strftime(".%#{fraction_digits}N")
    end
    s << (utc? ? 'Z' : strftime("%:z"))
  end

同样,关于 to_s 我也查看了对应的源代码(这部分代码写在 C 里,是拿不到 source 的),同样会根据是否是 utc 时间来返回不同的格式。

  static VALUE
time_to_s(VALUE time)
{
    struct time_object *tobj;

    GetTimeval(time, tobj);
    if (TIME_UTC_P(tobj))
        return strftimev("%Y-%m-%d %H:%M:%S UTC", time, rb_usascii_encoding());
    else
        return strftimev("%Y-%m-%d %H:%M:%S %z", time, rb_usascii_encoding());
}
jzlikewei closed this topic. 25 Aug 23:09
jzlikewei reopened this topic. 25 Aug 23:10

JSON 中时间对象的输出格式是怎么定义的?

最后的参考是 rails 的实现:ActiveSupport/TimeWithZone.html#method-i-as_json

You need to Sign in before reply, if you don't have an account, please Sign up first.