Proposal Details

The new package json/v2 has format options unix, unixmilli, unixmicro, and unixnano for time.Time, that encode the time as float of seconds, milliseconds, microseconds, and nanoseconds since epoch. The encoded float has nanosecond precision for each unit.

I propose that we change the format options unix, unixmilli, unixmicro, and unixnano to emit integer timestamps, rather than floats.

Motivation

Many APIs that use epoch timestamps use integers epoch timestamps. High precision floats are not commonly used due to rounding issues in clients.

The time package has the time.Time-methods Unix, UnixMilli, UnixMicro, and UnixNano that return integer values. Equally named json/v2 format options should have the same logic to avoid confusion.

time.Time json/v2 format
Unix() int64 format:unix
UnixMilli() int64 format:unixmilli
UnixMicro() int64 format:unixmicro
UnixNano() int64 format:unixnano

Example of Current Behavior (Playground)

type times struct {
    Time  time.Time `json:"time,format:unix"`
    Time2 time.Time `json:"time2,format:unixmilli"`
    Time3 time.Time `json:"time3,format:unixmicro"`
    Time4 time.Time `json:"time4,format:unixnano"`
}

enc, _ := json.Marshal(times{
    Time:  time.Unix(123, 456),
    Time2: time.Unix(123, 456),
    Time3: time.Unix(123, 456),
    Time4: time.Unix(123, 456),
})
fmt.Print(string(enc))
// Output:
// {"time":123.000000456,"time2":123000.000456,"time3":123000000.456,"time4":123000000456}

Comment From: gabyhelp

Related Issues

Related Code Changes

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

Comment From: dsnet

Here are the pros and cons of this.

Benefits * unix, unixmilli, unixmicro, unixnano matches the precision of the existing Unix, UnixMilli, UnixMicro, and UnixNano methods.

Detriments * You cannot express a Unix timestamp in seconds with sub-second precision. A number of JSON APIs actually accept Unix timestamps with sub-second precision. * Timestamps do not roundtrip through JSON marshal and unmarshal because you lose precision. Though this is already true for other formats such as RFC3339 (which is only second resolution and RFC3339Nano is with sub-second resolution).

Note that the behavior today (having fractional precision by default) can be turned into a non-fractional integer by always calling time.Time.Round on the Go struct field prior to marshaling. However, if we remove the encoding of fractional component, there is no way for someone to maintain precision when they need it. I feel strongly that we still need a way to preserve precise timestamps for those who need (and we are already depending on precise timestamps of this in production).

If we change this, we would also need something like unixprecise, unixmilliprecise, unixmicroprecise, unixnano, or something.

Comment From: dsnet

Perhaps this proposal is too narrow in scope. There are often times where you want to adjust a time.Time before serializing. Another reasonable alteration is change the location.

One could consider the ability to specify something like format:unix,round:1s (format as Unix seconds, but round to seconds, which would accomplish what this proposal desires) or format:RFC3339Nano,location:UTC (format as RFC 3339, but always encode as UTC to avoid leaking our general location). This would provide greater flexibility beyond what this particular proposal is suggesting.

Comment From: lmittmann

One could consider the ability to specify something like format:unix,round:1s (format as Unix seconds, but round to seconds, which would accomplish what this proposal desires) or format:RFC3339Nano,location:UTC (format as RFC 3339, but always encode as UTC to avoid leaking our general location). This would provide greater flexibility beyond what this particular proposal is suggesting.

This seems like a lot of complexity. A potentially more straight forward solution might be to support customizable formatting behavior via https://github.com/golang/go/issues/71664. This way one could change the behaviour of e.g. the unix format.

Comment From: feeeei

I still believe the format:unix family should return an int/int64 type, not float.

Relying on existing production usage of this behavior shouldn’t justify the design. Even if fractional seconds are preserved, the default precision remains contentious: some systems log 3 decimal places, others use 6, while Unix timestamps natively support 9 (nanoseconds).

Additionally, returning float creates inconsistency with Go’s own time.Unix() family of methods. Switching between float and int in everyday code would feel disjointed and harm developer ergonomics.

My suggestion: Default to returning int64 for Unix timestamps, while providing an optional mechanism to control fractional precision.

@dsnet