Skip to content

proposal: encoding/json/v2: decide on default representation for time.Duration #71631

Closed

Description

@dsnet

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.
    • ❌ Representation of small durations is verbose (e.g., 1ns would be 0.000000001 as opposed to "1ns" from time.Duration.String).
    • ❌ Parsing this representation into a time.Duration using v1 "encoding/json" may not result in an error, leading to silent data corruption. For example, 1 is a valid representation for 1 second, but parsing this into time.Duration with 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.Duration and 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.Duration since 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.
    • ❌ Representation of small durations is verbose (e.g., 1ns would be "PT0.000000001S" as opposed to "1ns" from time.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" from time.Duration.String).

Activity

  1. added this to the Proposal milestone on Feb 9, 2025
  2. dsnet commented on Feb 9, 2025

    @dsnet
    MemberAuthor

    Please vote for what you believe is the most reasonable default representation of time.Duration in v2:

    • 👍 for JSON integer in nanoseconds (e.g., 47683228428000)
    • 😆 for JSON fractional number in seconds (e.g., 47683.228428)
    • 🎉 for JSON string using time.Duration.String (e.g., "13h14m43.228428s")
    • ❤ for JSON string using restricted ISO 8601 (e.g., "PT13H14M43.228428S")
    • 🚀 for JSON string using base60 representation (e.g., "13:14:43.228428")
  3. timbray commented on Feb 9, 2025

    @timbray

    Two reasons for ❤:

    1. I think most programming languages have 8601 libraries that would read this correctly out of the box.
    2. It seems weird that the time representation is 8601-based but the duration representation isn't.
  4. dsnet commented on Feb 9, 2025

    @dsnet
    MemberAuthor

    @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?

  5. timbray commented on Feb 9, 2025

    @timbray

    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.

  6. added
    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a tool
    on Feb 9, 2025
  7. doggedOwl commented on Feb 9, 2025

    @doggedOwl

    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.

  8. seankhliao commented on Feb 9, 2025

    @seankhliao
    Member

    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.

  9. doggedOwl commented on Feb 9, 2025

    @doggedOwl

    @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.

  10. puellanivis commented on Feb 9, 2025

    @puellanivis
    Contributor

    Absolutely, there will be a V1 marshaller option no matter what that implements the V1 behavior.

  11. seankhliao commented on Feb 9, 2025

    @seankhliao
    Member

    you can only do that if you only consume external messages in the old format, what about producing messages for external systems?

  12. doggedOwl commented on Feb 9, 2025

    @doggedOwl

    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.

  13. 71 remaining items

  14. moved this from Needs triage to Needs review in json/v2 working groupon Nov 22, 2025
  15. moved this from Needs review to Under review in json/v2 working groupon Dec 11, 2025
  16. neild commented on Dec 11, 2025

    @neild
    Contributor

    Discussed in the v2 working group.

    JSONv2 permits specifying the format to use when marshaling a time.Duration: format:sec for fractional seconds, format:iso8601 for 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.Duration use 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 from encoding/json.Marshal to encoding/json/v2.Marshal may 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/v2 should require the user to specify a format. The user must provide a format: option for time.Duration fields, and encoding/json/v2.Marshal will return an error if one is not present.

    We discussed whether encoding/json/v2.Unmarshal should accept the old default format (nanoseconds) when no format is specified. We concluded that it was better for Marshal and Unmarshal to be consistent, and that Unmarshal should also return an error when no format is specified for a duration.

    We also discussed whether Unmarshal should accept multiple formats, even when the field format specifies a single one. We could say, for example, that Unmarshal will 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/v2 should require a format for time.Duration values 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 uses encoding/json/v2 instead of encoding/json, we would be less concerned about users migrating to the v2 package.

    To summarize:

    • encoding/json.Marshal and encoding/json.Unmarshal behave as they always have, even when using the JSONv2 backend.
    • encoding/json/v2.Marshal and encoding/json/v2.Unmarshal will return an error when provided with a time.Duration that does not have a format (such as format:sec or format:iso8601) specified.
    • We're open to adding a default format for durations in the future.
  17. moved this from Under review to Done in json/v2 working groupon Dec 18, 2025
  18. closed this as completedby moving to Done in json/v2 working groupon Dec 18, 2025
  19. moved this from Done to Under review in json/v2 working groupon Dec 18, 2025
  20. moved this from Under review to Done in json/v2 working groupon Jan 8, 2026
  21. gopherbot commented on Jan 9, 2026

    @gopherbot
    Contributor

    Change https://go.dev/cl/735080 mentions this issue: encoding/json/v2: remove issue reference in Duration formatting error

  22. andrewhodel commented on Mar 26, 2026

    @andrewhodel

    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.

    units is much easier to work with than nanoseconds and although there is no default and there is a requirement of setting a default, units should be the default.

  23. gopherbot commented on Sep 21, 2026

    @gopherbot
    Contributor

    Change https://go.dev/cl/835965 mentions this issue: encoding/json: remove stale TODOs for time.Duration representation

  24. gopherbot commented on Sep 23, 2026

    @gopherbot
    Contributor

    Change https://go.dev/cl/837485 mentions this issue: encoding/json: restore the time.Duration difference test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a toolProposal

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions