Repository navigation
proposal: encoding/json/v2: decide on default representation for time.Duration #71631
Description
Proposal Details
This is a sub-issue of the "encoding/json/v2" proposal (#71497).
Here, we discuss the default JSON representation of time.Duration. In #71497, we proposed using the time.Duration.String representation and continue to propose using that format, but want to provide an avenue for the community to find concensus on the right format if it should be something else. To be clear, v2 supports multiple formats, but the focus of this issue is determining what the right default is.
The following is an overview of reasonable representations of a duration and their strengths and weaknesses. There is no obviously right approach. All have flaws of some kind. A major part of the problem is the lack of a standard like RFC 3339 for representing time durations.
-
JSON integer in nanoseconds (e.g.,
47683228428000)- ✔️ It's what "encoding/json" does today.
- ❌ It lacks any context as to its meaning. Is it in seconds, milliseconds, microseconds, nanoseconds? Lacking context, seconds is often a reasonable and incorrect first guess.
- ❌ It can't easily scale to higher precisions as it arbitrarily assumes only nanosecond resolution.
- Two decades ago, most APIs used microseconds until CPUs got faster and then nanoseconds became more popular, leading to inconsistency where some APIs still operated on microseconds, and yet others on nanoseconds. CPU frequency has plateaued since the end of Dennard scaling around 2006, and it seems nanosecond resolution is here to stay for some time. However, it's still entirely possible that some breakthrough technology brings clock speeds higher and then we're suddenly talking about durations at picosecond resolution.
- ❌ It exceeds the exact representation of float64 in ~104 days, which will reduce accuracy on JSON parsers without real 64-bit integer support.
-
JSON fractional number in seconds (e.g.,
47683.228428)- ✔️ The presence of a fractional component is a strong signal that this is probably in seconds.
- ✔️ It can scale to arbitrary precision by just adding more fractional digits.
- ❌ Its representation in float64 is lossy. For example,
time.Duration(strconv.ParseFloat("0.000000015", 64)*1e9)results in 14ns (rather than the expected 15ns).- If one correctly does
time.Duration(math.Round(strconv.ParseFloat(...)*1e9)), then they get the right answer more often, but still run into precision errors eventually (~104 days). Also, the more correct parsing is less intuitive than the more trivial parsing.
- If one correctly does
- ❌ Representation of small durations is verbose (e.g., 1ns would be
0.000000001as opposed to"1ns"fromtime.Duration.String). - ❌ Parsing this representation into a
time.Durationusing v1 "encoding/json" may not result in an error, leading to silent data corruption. For example,1is a valid representation for 1 second, but parsing this intotime.Durationwith v1 will result in 1 nanosecond without an error. This may lead to significant interoperability issues between v1 and v2.
-
JSON string using
time.Duration.String(e.g.,"13h14m43.228428s")- ✔️ The meaning is unambiguous to a human reader.
- ✔️ The representation is exact (in that there's no accidental loss due to float64 handling of JSON parsers).
- ✔️ It can scale to arbitrary precision by adjusting the SI prefix (e.g.,
"1ps"for 1 picosecond) or by using more fractional digits. - ✔️ There is built-in formatting and parsing of this in Go.
- ✔️ "log/slog" already uses this representation for
time.Durationand there is argument for consistency. - ❌ It's fairly Go specific. The use of "m" for minute may be considered non-standard, since the SI symbol for minute is actually "min", while "m" is usually the unit for meters.
-
JSON string using ISO 8601 (e.g.,
"PT13H14M43.228428S")- ✔️ The meaning is unambiguous to a human reader.
- ✔️ The representation is exact (in that there's no accidental loss due to float64 handling of JSON parsers).
- ✔️ It can scale to arbitrary precision by using more fractional digits.
- ✔️ It would be more consistent with the formatting of
time.Time, which uses RFC 3339, which is a subset of ISO 8601. - ✔️ ISO 8601 is probably the closest thing to a widely accepted standard for duration formatting.
- ❌ ISO 8601 is not a specific grammar.
- Contrary to popular belief, ISO 8601 does not specify a particular grammar, but a family of grammars and lets selection of a particular grammar be "by agreement between the communicating parties". Thus, Go would still need to choose a particular grammar to abide by. The most reasonable flavor of ISO 8601 might be the grammar used by JavaScript as specified in TC39. JSON finds its heritage in JavaScript, so using JavaScript's grammar as the basis is reasonable. However, TC39's grammar permits duration units like "years", "months", "weeks", etc., but does not define the exact quantity of time for such units. Consequently, Go cannot accurately convert "P1Y" (i.e., 1 year) into a
time.Durationsince the length of a year is ill-defined. At best, Go can use a subset of TC39's grammar that's constrained to only the units like hours, minutes, and seconds.
- Contrary to popular belief, ISO 8601 does not specify a particular grammar, but a family of grammars and lets selection of a particular grammar be "by agreement between the communicating parties". Thus, Go would still need to choose a particular grammar to abide by. The most reasonable flavor of ISO 8601 might be the grammar used by JavaScript as specified in TC39. JSON finds its heritage in JavaScript, so using JavaScript's grammar as the basis is reasonable. However, TC39's grammar permits duration units like "years", "months", "weeks", etc., but does not define the exact quantity of time for such units. Consequently, Go cannot accurately convert "P1Y" (i.e., 1 year) into a
- ❌ Representation of small durations is verbose (e.g., 1ns would be
"PT0.000000001S"as opposed to"1ns"fromtime.Duration.String).
-
JSON string using base60 representation (e.g.,
"13:14:43.228428")- ✔️ This is the reading you often see when reading a stop-watch or also defacto used by popular programs (e.g. ffmpeg), so there's some precedence for this representation outside of Go.
- ✔️ The meaning is decently unambiguous to a human reader.
- ❌ Although, it is still ambiguous whether this represents a wall-clock reading or a duration.
- ✔️ The representation can be exact (in that there's no accidental loss due to float64 handling of JSON parsers).
- ✔️ It can scale to arbitrary precision by just adding more fractional digits.
- ❌ To my knowledge, there's no standard specification for this.
- ❌ There's no native formatter or parser for this in Go.
- ❌ Representation of small durations is verbose (e.g., 1ns would be
"0:00:00.000000001"as opposed to"1ns"fromtime.Duration.String).
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsIncoming
- StatusShow more project fieldsDone
Activity
dsnet commentedon Feb 9, 2025
Please vote for what you believe is the most reasonable default representation of
time.Durationin v2:47683228428000)47683.228428)time.Duration.String(e.g.,"13h14m43.228428s")"PT13H14M43.228428S")"13:14:43.228428")timbray commentedon Feb 9, 2025
Two reasons for ❤:
dsnet commentedon Feb 9, 2025
@timbray, you have experience writing RFCs, would you be interested in writing one similar to RFC 3339 that standardizes on a particular profile of ISO 8601 so that we can settle this problem of too many ISO 8601-like grammars that are all subtly different from each other?
gabyhelp commentedon Feb 9, 2025
Related Issues
Related Code Changes
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
timbray commentedon Feb 9, 2025
It's about midnight here now so I'll get back to you. I can probably write an Internet-Draft such as you propose. Nobody can “write an RFC”, to turn an Internet-Draft into an RFC you have to go through the IETF process which is fairly sane but not fast. Having said that, an Internet-Draft is immutable and lives forever and even though the front matter says it shouldn't be used as a reference, that happens regularly.
doggedOwl commentedon Feb 9, 2025
There is very little difference from the time.duration.string and the subset of ISO 8601 so it is worth to go the distance and make it the default.
One of the main uses of json is in communication with front-end and thus JS.
Having out of the box compatibility with that is good thing for a small deviation from the specific go time package.
seankhliao commentedon Feb 9, 2025
Since JSON is a serialization format, what does interoperability look like for other software, like those built on earlier versions of Go, or expecting to parse a field with the old representation?
From what I can tell, whether it's default or specified by the new struct tag, the choice of format affects both encoding and decoding. It doesn't seem possible to migrate any existing system from one format to another.
I view this as similar to #10275 , whether the change is to the underlying type or to just encoding/json/v2 , it has potential to break systems without careful auditing, especially with external types which may add in new struct tags.
doggedOwl commentedon Feb 9, 2025
@seankhliao If I understood the parent proposal right, you would create an Unmarshaller with V1 options, and leave the Marshaler with default options. Anyway your question does not have anything to do with this default which in anycase would have been different in the original proposal. The v2 proposal containes several other breaking changes and their proposed migration options.
puellanivis commentedon Feb 9, 2025
Absolutely, there will be a V1 marshaller option no matter what that implements the V1 behavior.
seankhliao commentedon Feb 9, 2025
you can only do that if you only consume external messages in the old format, what about producing messages for external systems?
doggedOwl commentedon Feb 9, 2025
json v1 will be entirely implemented in v2 with the right compativbility options so you can both consume and create messages according to the v1 spec using v2.
71 remaining items
neild commentedon Dec 11, 2025
Discussed in the v2 working group.
JSONv2 permits specifying the format to use when marshaling a
time.Duration:format:secfor fractional seconds,format:iso8601for ISO 8601 using hours/minutes/seconds, etc. The question here is what we should do when marshaling or unmarshaling a duration which does not have a user-provided format.JSONv1 marshals durations as integer nanoseconds. We feel that this was a clear mistake: There's no indication of what the unit is, making unit conversion errors too easy. Also, nanoseconds overflow float64 in only 104 days.
The world seems to be converging on ISO 8601 (or some subset of it). As two examples, ECMAScript's Temporal proposal and
java.time.Durationuse ISO 8601 as the string representations of durations. If we were starting from scratch, then this seems like the right choice.However, changing representations silently is hazardous. If we change the default representation of
time.Duration, then users switching fromencoding/json.Marshaltoencoding/json/v2.Marshalmay be unexpectedly broken by the change.Our conclusion is that we don't want to keep the old default (nanoseconds), but we also don't want to silently change the representation of durations. Therefore,
encoding/json/v2should require the user to specify a format. The user must provide aformat:option fortime.Durationfields, andencoding/json/v2.Marshalwill return an error if one is not present.We discussed whether
encoding/json/v2.Unmarshalshould accept the old default format (nanoseconds) when no format is specified. We concluded that it was better forMarshalandUnmarshalto be consistent, and thatUnmarshalshould also return an error when no format is specified for a duration.We also discussed whether
Unmarshalshould accept multiple formats, even when the field format specifies a single one. We could say, for example, thatUnmarshalwill always accept ISO 8601 because it can be unambiguously distinguished from any other format. We all agreed that this is not something we should do by default: Lax parsing is a potential request smuggling vector and can cause unintentional dependence on a specific parser.While our conclusion is that
encoding/json/v2should require a format fortime.Durationvalues for now, this is something that we could change in the future. Adding a default is not a backwards incompatible change. Doing so would require evidence that the change will not cause harm; for example, if a time comes when most Go code usesencoding/json/v2instead ofencoding/json, we would be less concerned about users migrating to the v2 package.To summarize:
encoding/json.Marshalandencoding/json.Unmarshalbehave as they always have, even when using the JSONv2 backend.encoding/json/v2.Marshalandencoding/json/v2.Unmarshalwill return an error when provided with atime.Durationthat does not have a format (such asformat:secorformat:iso8601) specified.gopherbot commentedon Jan 9, 2026
Change https://go.dev/cl/735080 mentions this issue:
encoding/json/v2: remove issue reference in Duration formatting errorencoding/json/v2: remove issue reference in Duration formatting error
encoding/json/v2: remove issue reference in Duration formatting error
andrewhodel commentedon Mar 26, 2026
golang uses
Time.Duration.ToString()as the units format meaning there is nothing after hours (no days, years) but microseconds and nanoseconds are included. There's no reason this shouldn't become an RFC format.unitsis much easier to work with than nanoseconds and although there is no default and there is a requirement of setting a default,unitsshould be the default.types/jsonx: add wrapper types for JSON time and duration wire formats
gopherbot commentedon Sep 21, 2026
Change https://go.dev/cl/835965 mentions this issue:
encoding/json: remove stale TODOs for time.Duration representationgopherbot commentedon Sep 23, 2026
Change https://go.dev/cl/837485 mentions this issue:
encoding/json: restore the time.Duration difference test