feature: Option to serialize time.Time as wall clock millis/micros/nanos as integers
Is your feature request related to a problem? Please describe.
This package currently exposes several ways to serialize time.Time values:
type TimeMode int
const (
// TimeUnix causes time.Time to be encoded as epoch time in integer with second precision.
TimeUnix TimeMode = iota
// TimeUnixMicro causes time.Time to be encoded as epoch time in float-point rounded to microsecond precision.
TimeUnixMicro
// TimeUnixDynamic causes time.Time to be encoded as integer if time.Time doesn't have fractional seconds,
// otherwise float-point rounded to microsecond precision.
TimeUnixDynamic
// TimeRFC3339 causes time.Time to be encoded as RFC3339 formatted string with second precision.
TimeRFC3339
// TimeRFC3339Nano causes time.Time to be encoded as RFC3339 formatted string with nanosecond precision.
TimeRFC3339Nano
maxTimeMode
)
All of these options are either lossy w/r/t the time's wall clock time (everything but TimeRFC3339Nano) or dependent on the for-display-only Location component of the time. I believe this means there is no serialization option that guarantees that two times constructed using time.Micro(n) on different machines will serialize identically for all possible values of n, because the current numerical serialization options are lossy, and the RFC3339-based options are location dependent, so two machines with different locations will produce different serializations.
Describe the solution you'd like
I propose adding new options to serialize time.Time as 64-bit integers with milli/micro/nanosecond precision. These would correspond to serializing the time using theTime.UnixMilli, Time.UnixMicro, and time.UnixNano methods. These options would provide location-independent ways of serializing times that give clear precision guarantees, and don't require reasoning about how the range of times you plan to support will interact with rounding behavior of 64-bit floating point. By contrast, the existing floating-point TimeUnixMicro will only be lossless at microsecond precision only up to around 2^53 microseconds. Maybe that works for many applications but it seems hard to reason about.
Describe alternatives you've considered
I'm currently handling this issue in my application by serializing using TimeRFC3339Nano, and then calling .UTC() on any times I want to serialize, which ensures that the location-dependence doesn't cause breakage. This works, but it adds extra boilerplate and needs to be handled correctly at every site where I have a time I want to serialize with cbor. I'd prefer to have a blanket option to say "serialize times as integers with unambiguous millisecond/nanosecond precision".
Additional context Add any other context or screenshots about the feature request here.
I'm happy to work on this if there's interest in this feature.
@ssanderson Thank you for opening this issue!
I also prefer not to encode high-precision time into floating point.
All of these options are either lossy w/r/t the time's wall clock time (everything but TimeRFC3339Nano) or dependent on the for-display-only
Locationcomponent of the time.
All options use UTC when encoding to CBOR time (tag 1) with integer or float value. For example:
-
TimeUnixuses POSIX [TIME_T] in UTC to encode to integer with 1-second precision. -
TimeUnixMicrouses POSIX [TIME_T] in UTC to encode to float (supports fractional seconds).
To summarize my understanding of the issue:
-
TimeUnixis location independent and has clear precision guarantee, but it has no fractional seconds. -
TimeUnixMicrois location independent and supports fractional seconds, but float64 is too lossy for your use case. -
Given this, you would like to encode time with fractional seconds to CBOR time (tag 1) with integer value.
Unfortunately, I think RFC 8949 specifies 1-second precision for encoding to a CBOR time (tag 1) with integer value. For encoding fractional seconds, RFC 8949 specifies using CBOR time (tag 1) with a 64-bit floating point value.
Put another way: how would a generic CBOR decoder know if an encoded CBOR time (tag 1) with an integer value represents seconds, milliseconds, microseconds, or nanoseconds elapsed since UNIX Epoch?
I can take a look during a weekend (probably April 26-27) in case I'm mistaken.
Thanks again for opening this issue and please let me know if I missed anything.
@fxamacker thanks for the reply! Your understanding looks correct to me.
Put another way: how would a generic CBOR decoder know if an encoded CBOR time (tag 1) with an integer value represents seconds, milliseconds, microseconds, or nanoseconds elapsed since UNIX Epoch?
Hmm. That's a good question. I hadn't realized the existing options were standard options defined by an RFC. For the use-case I'm targeting I control both the encoder and the decoder, so I was assuming that both would need to specify the same EncOptions and DecOptions. I agree that this seems like not a good solution though if the time is still serialized using the standard tag for time.
One option might be to add the ability to set a custom non-standard tag to be used when serializing times (e.g. EncOptions.CustomTimeTag), and to specify that the time modes I've proposed above would only be allowed if you've also specified a custom time tag. Though at that point it might make sense to wait for a more general custom serialization feature like https://github.com/fxamacker/cbor/issues/623 if that's likely to happen in the medium/long term. Though there might still be reason to special-case time given that it's commonly-used and it's already covered by the CBOR rfc.
@ssanderson Yes, I am looking into issue #623 and waiting for the Go team to finalize the proposed API.
Currently, if users want to encode time in ways that are not defined in RFC 8949 (CBOR), they can use CBOR tag numbers not assigned by IANA and implement MarshalCBOR and UnmarshalCBOR.
This library allows users to specify a CBOR tag number that isn't assigned by IANA to "extend" CBOR.
For example, users can use tag number 55 and declare UnixNanoTime type for time.Time:
const unixNanoTimeTagNum = 55 // CBOR tag number 55 (not assigned by IANA as of April 26, 2025)
// UnixNanoTime represents time.Time for encoding to an unassigned CBOR tag number
// with an integer content representing elapsed nanoseconds since UNIX Epoch UTC.
type UnixNanoTime time.Time
For UnixNanoTime, users can implement MarshalCBOR and UnmarshalCBOR.
NOTE: They will automatically be called by Marshal, Unmarshal, etc. in this library.
var _ cbor.Marshaler = &UnixNanoTime{}
var _ cbor.Unmarshaler = &UnixNanoTime{}
// MarshalCBOR encodes UnixNanoTime to #6.55(int).
func (t UnixNanoTime) MarshalCBOR() (data []byte, err error) {
nsec := time.Time(t).UnixNano()
return cbor.Marshal(cbor.Tag{Number: unixNanoTimeTagNum, Content: nsec})
}
// etc.
Full program: https://go.dev/play/p/qh93FpWDdwM
Although this approach uses a user-defined type to wrap time.Time, the code is only needed in one place and it allows fine-grained control over how time is encoded and decoded.
Please let me know if this works for you.
@fxamacker that would be a workable solution, though it's a bit annoying to have to use a newtype wrappe around time.Time, as Go doesn't make that super convenient. What I'm currently doing is just ensuring that I call .UTC() on all the times I emit, which strips the locale information and normalizes the timezone info, and then I'm using TimeRFC3339Nano for encOpts.Time.
One other option that occurs to me looking at this again: what would you think about adding a TimeRFC3339NanoUTC option, with the same beahvior as the current TimeRFC3339Nano but applies .UTC() as well? I think that would produce a fully-lossless standard-compliant serialization option. The main downside of that is just it's relatively space-inefficient, but that's probably ok for many use-cases.
+1, I would love to see direct time.Time to Nano precision and UTC.
One other option that occurs to me looking at this again: what would you think about adding a TimeRFC3339NanoUTC option, with the same beahvior as the current TimeRFC3339Nano but applies .UTC() as well?
Sounds great! I opened #687 and will implement TimeRFC3339NanoUTC it this weekend (probably Saturday, July 19). It will be included in next release (v2.10).
This feature was suggested by @ssanderson at:
- https://github.com/fxamacker/cbor/issues/656#issuecomment-2847684876
Currently, we have TimeRFC3339Nano option for encoding to CBOR time (tag 0) with nanosecond precision. However, it retains the timezone of time.Time being encoded and it can be inconvenient to set time.Time to UTC before encoding it to CBOR. ...
Sorry this took a while, I wanted to look into a different solution before deciding (it is still on the table after v2.10 is released).
Closed by issue #687 and we can continue discussion there.