Skip to content

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

Open
@dsnet

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

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

    Status

    Incoming

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions