Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: encoding/json/v2: use "72h3m0.5s" as the default representation for time.Duration #71631

Open
dsnet opened this issue Feb 9, 2025 · 49 comments
Labels
LibraryProposal Issues describing a requested change to the Go standard library or x/ libraries, but not to a tool Proposal
Milestone

Comments

@dsnet
Copy link
Member

dsnet commented Feb 9, 2025

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).
@dsnet dsnet added the Proposal label Feb 9, 2025
@gopherbot gopherbot added this to the Proposal milestone Feb 9, 2025
@dsnet
Copy link
Member Author

dsnet commented Feb 9, 2025

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

@timbray
Copy link

timbray commented Feb 9, 2025

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.

@dsnet
Copy link
Member Author

dsnet commented 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
Copy link

gabyhelp commented Feb 9, 2025

@timbray
Copy link

timbray commented 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.

@gabyhelp gabyhelp added the LibraryProposal Issues describing a requested change to the Go standard library or x/ libraries, but not to a tool label Feb 9, 2025
@doggedOwl
Copy link

doggedOwl commented 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
Copy link
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.

@doggedOwl
Copy link

doggedOwl commented 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
Copy link

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

@seankhliao
Copy link
Member

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

@doggedOwl
Copy link

doggedOwl commented 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.

@mpx
Copy link
Contributor

mpx commented Feb 10, 2025

It might be worth reviewing what other JSON serialisation implementations do with durations. Eg, Protobuf / ProtoJSON:

The JSON representation for Duration is a String that ends in s to indicate seconds and is preceded by the number of seconds, with nanoseconds expressed as fractional seconds.

"1.000340012s", "1s"

I think this option adding to the list above (but it won't get the voting attention of the initial options). It could be considered a subset of time.Duration.String that is less Go specific and simpler to encode/decode.

@dsnet
Copy link
Member Author

dsnet commented Feb 10, 2025

While I find ISO 8601 less readable compared to the Go time.Duration.Format, I concede that it's the closest thing to a worldwide standard, but one that lacks a singular grammar. My greatest hesitation with having v2 use ISO 8601 is claiming support for it and having inevitable bugs filed that claim that we are not in compliance with it in some optional area.

My motivation with an RFC is to have something similar to RFC 3339 to specify a limited and concise grammar that the Internet agrees as the proper way to represent timestamps in order to resolve interoperability issues (which is the stated goal of why it was created and has largely accomplished its goal). When bugs are filed claiming problems parsing a timestamp, we can look at RFC 3339 to settle the issue. We need something similar for durations.

I can probably write an Internet-Draft such as you propose.

I'd be happy to help write the draft, but I'm unfamiliar to the RFC process. I would expect the RFC to satisfy several goals:

  1. Be freely accessible as ISO 8601 is not
  2. Choose a subset of ISO 8601 and make opinionated decisions about many optional grammars that are "determined by the interchange parties"
  3. Capable of precisely measuring the exact duration of time in fractional seconds (which suggests forbidding units like years and months).

Many APIs or protocols implicitly support a limited grammar of ISO 8601 by functionally banning higher order units (e.g., years), banning commas, requiring adherence to "carry-over points" (or not), etc. However, exactly what is supported or not often isn't documented.

RFC 3339 was written in 2002 for timestamps, and surprisingly we haven't had the equivalent for durations. I suspect an RFC for duration to be much simpler than RFC 3339 since a limited grammar is actually fairly simple.

As a historical note: #43823 altered the Go "time" package to support commas to be in greater compliance with ISO 8601, which sadly broke Go's compliance with RFC 3339, which forbids commas. By transitive property, this bug persists in v1 "encoding/json" as fixing it unfortunately broke many users who were implicitly relying on commas in timestamps. In v2, we want to aim to have strict and precise support for various formats to support JSON's goal being a language agnostic, data interchange, format.

@dsnet
Copy link
Member Author

dsnet commented Feb 10, 2025

Regarding protobuf's format for a duration, I'll point out that they're also in an inconsistent position where timestamps use RFC 3339, while durations use a custom format. The choice of format for duration predates my time working on protobufs, but I suspect it's because RFC 3339 existed, but an equivalent RFC for durations did not.

@dsnet
Copy link
Member Author

dsnet commented Feb 10, 2025

@seankhliao

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?

At present, we're assuming that software that's already been serializing a time.Duration as nanoseconds will likely be required to continue doing so. To migrate to using v2, such fields can be marked with format:nano to opt into the original v1 behavior.

That said, some closed systems (e.g., RPCs between two internal services) could theoretically migrate the wire representation since both sides of the RPC are controlled by the same organization. One could imagine a format grammar that specifies dual support for formats. For example, suppose there was format:'units|nano' where the duration is marshaled using time.Duration.String, but unmarshaled from either time.Duration.Parse or as an integer containing nanoseconds. This would be necessary as an intermediate migration step since you can't migrate both ends of the service simultaneously.

This particular issue is about the default representation for time.Duration, but we could figure out some way to support decoding from multiple formats in a future proposal after the initial release of v2.

@timbray
Copy link

timbray commented Feb 10, 2025

would you be interested in writing one similar to RFC 3339 that standardizes on a particular profile of ISO 8601

Having checked around… what a mess. So yes, if you guys decide to adopt the ISO8601 profile approach, I would volunteer to write an Internet-Draft in the style of 3339 with the selected profile. I am not repeat NOT volunteering to try to push this through the IETF and make it an RFC. (There's a small but non-zero chance that enough people would be interested that this could end up happening anyhow.)

This might be useful in that it would force us to be super-precise. What I'd want before I started working on this would be an agreed-on list of example duration literals that you would expect to be accepted, and another list that would be rejected as ill-formed. Building specs from examples is so much easier than from documents explaining what we think we want.

@timbray
Copy link

timbray commented Feb 10, 2025

I'd be happy to help write the draft, but I'm unfamiliar to the RFC process. I would expect the RFC to satisfy several goals:

Oops, wrote the above before I caught up on this morning's traffic. Yes, your 1-3 list seems sensible and achievable, and I agree that it would be much shorter than 3339, among other things almost all the ABNF needed can be taken from 3339 by reference. But I'd still want to see a list of good/bad literals to make sure thinking is clear.

@prattmic
Copy link
Member

It can't easily scale to higher precisions as it arbitrarily assumes only nanosecond resolution.

This is a very minor part of the proposal, but I will note that time.Duration is explicitly documented as "A Duration represents the elapsed time between two instants as an int64 nanosecond count." Because Duration is an int64, code can and does directly cast to and from Duration, assuming the value is nanoseconds.

Thus I don't realistically see how time.Duration could ever be made to allow higher precision. I think that would need to be a new type.

@dsnet
Copy link
Member Author

dsnet commented Feb 10, 2025

I don't realistically see how time.Duration could ever be made to allow higher precision. I think that would need to be a new type.

True, but we want Go's implementation of a duration in JSON to be forwards compatible if there's ever a "time/v2" that supports higher precision.

@dsnet
Copy link
Member Author

dsnet commented Feb 10, 2025

Here's my attempt at a particular profile of ISO 8601 that I believe should be parsable by most compliant ISO 8601 implementations.

minus       = %x2D                             ; -
digit1-5    = %x31-35                          ; 1-5
digit1-9    = %x31-39                          ; 1-9
digit0-9    = %x30-39                          ; 0-9
b60-int     = ( digit1-5 digit0-9 / digit1-9 ) ; 1-59
pos-int     = digit1-9 *digit0-9
dur-zero    = "PT0S"
dur-secfrac = "." 0*digit0-9 digit1-9
dur-second  = ( b60-int [ dur-secfrac ] / "0" dur-secfrac ) "S"
dur-minute  = b60-int "M" [ dur-second ]
dur-hour    = pos-int "H" [ ( dur-minute / dur-second ) ]
dur-time    = "T" (dur-hour / dur-minute / dur-second )
duration    = ( [ minus ] "P" dur-time ) / dur-zero
Examples

Valid:

  • PT0S
  • PT1S
  • PT0.000000001S
  • PT0.123S
  • PT59.123S
  • PT1M59.123S
  • PT1M
  • PT1H59M59S
  • PT1H59M59.123S
  • PT1H59.123S
  • PT1H
  • -PT12356H59M59.123S

Invalid:

  • P1Y
  • P1M
  • P1D
  • PT09.123S
  • PT05M09.123S
  • PT0H
  • PT1H0M
  • PT1H0S
  • PT1H0M0S
  • PT0M
  • PT0.123H
  • PT0.123M
  • PT0.000S
  • -PT0S

Design considerations for the grammar:

  • We aim to be parsable by most profiles of ISO 8601.
  • We disallow leading zeros for minutes and seconds since ISO 8601 does not require that parsers actually support leading zeros. (ISO 8601 only requires handling of leading zeros if an element has a "defined length", but is silent about leading zeros for elements without a defined length as is the case for duration components).
  • We disallow use of years, months, weeks, and days since the exact length of time for such units is ill-defined.
  • We only allow fraction components on seconds since ISO 8601 specifies that only the "lowest order component may have a decimal fraction". We do not specify a limit on the precision of the sub-fraction, though implementations may choose to reject overly precise durations.
  • We do not specify a limit on the maximum number of hours, though implementations may choose to reject overly large durations.
  • We always elide designators with a length of zero (except for when the entire duration is zero) since this is how most ISO 8601 formatters seem to operate (but is not required by ISO 8601).
  • We adhere to "carry-over points" for minutes and seconds since some ISO 8601 profiles may expect this.
  • We only permit "." as the sub-second separator (similar to RFC 3339).
  • We only permit uppercase designators (similar to RFC 3339). Using lowercase designators might obtain some of the readability benefits of time.Duration.String (e.g., PT13h14m43.228428s), but some ISO 8601 profiles reject lowercase designators.
  • We permit a leading minus sign to indicate a negative duration. This technically violates ISO 8601, but is a practical necessity. For example, TC39 for JavaScript also allows negative durations for their ISO 8601 implementation.
  • This grammar happens to be a bijective representation for any finite duration value.

If we're willing to lose the bijective property, here are some reasonable ways to loosen the parsing behavior and still be ISO 8601 compliant:

  • Permit unnecessary zero-length component (e.g., PT0H0M0S would be valid).
  • Permit unnecessary trailing zeros in sub-fraction (e.g., PT0.000S would be valid).
  • Drop "carry-over" requirement for minute and second components (e.g., PT123456789S would be valid). However, some profiles might enforce adherence to "carry-over points".

We define that:

  • There are exactly 60 seconds in a minute (no leap seconds).
  • There are exactly 60 minutes in an hour.

With the above grammar, it is possible represent any finite duration.

@daenney
Copy link

daenney commented Feb 11, 2025

We disallow use of years, months, weeks, and days since the exact length of time for such units is ill-defined.

XML's duration format made this mistake. It's defined as:

[6]   duYearFrag ::= [unsignedNoDecimalPtNumeral](https://www.w3.org/TR/xmlschema11-2/#nt-unsNoDecNuml) 'Y'
[7]   duMonthFrag ::= [unsignedNoDecimalPtNumeral](https://www.w3.org/TR/xmlschema11-2/#nt-unsNoDecNuml) 'M'
[8]   duDayFrag ::= [unsignedNoDecimalPtNumeral](https://www.w3.org/TR/xmlschema11-2/#nt-unsNoDecNuml) 'D'
[9]   duHourFrag ::= [unsignedNoDecimalPtNumeral](https://www.w3.org/TR/xmlschema11-2/#nt-unsNoDecNuml) 'H'
[10]   duMinuteFrag ::= [unsignedNoDecimalPtNumeral](https://www.w3.org/TR/xmlschema11-2/#nt-unsNoDecNuml) 'M'
[11]   duSecondFrag ::= ([unsignedNoDecimalPtNumeral](https://www.w3.org/TR/xmlschema11-2/#nt-unsNoDecNuml) | [unsignedDecimalPtNumeral](https://www.w3.org/TR/xmlschema11-2/#nt-unsDecNuml)) 'S'
[12]   duYearMonthFrag ::= ([duYearFrag](https://www.w3.org/TR/xmlschema11-2/#nt-duYrFrag) [duMonthFrag](https://www.w3.org/TR/xmlschema11-2/#nt-duMoFrag)?) | [duMonthFrag](https://www.w3.org/TR/xmlschema11-2/#nt-duMoFrag)
[13]   duTimeFrag ::= 'T' (([duHourFrag](https://www.w3.org/TR/xmlschema11-2/#nt-duHrFrag) [duMinuteFrag](https://www.w3.org/TR/xmlschema11-2/#nt-duMiFrag)? [duSecondFrag](https://www.w3.org/TR/xmlschema11-2/#nt-duSeFrag)?) | ([duMinuteFrag](https://www.w3.org/TR/xmlschema11-2/#nt-duMiFrag) [duSecondFrag](https://www.w3.org/TR/xmlschema11-2/#nt-duSeFrag)?) | [duSecondFrag](https://www.w3.org/TR/xmlschema11-2/#nt-duSeFrag))
[14]   duDayTimeFrag ::= ([duDayFrag](https://www.w3.org/TR/xmlschema11-2/#nt-duDaFrag) [duTimeFrag](https://www.w3.org/TR/xmlschema11-2/#nt-duTFrag)?) | [duTimeFrag](https://www.w3.org/TR/xmlschema11-2/#nt-duTFrag)

The "date" part of that format has a huge number of problems, like a duration of 3 months (P3M) can be a mixture of 28, 29, 30 or 31 days and you have no way to know unless you know the reference time to apply the duration to (3 months from May 2024) and have an up to date timezone database. Similarly, 2 years (P2Y) can be varying amount of days due to leap years without further context.

The time section of the XML duration format is identical to what @dsnet is proposing. This is extremely convenient for services that once started out with XML but have grown JSON interfaces over time. The exception is that the XML format does not support fractional seconds. So for interoperability sake, it would be great to not support the following:

Permit unnecessary trailing zeros in sub-fraction (e.g., PT0.000S would be valid).

That would ensure that 1s always gets serialised as PT1S and can pass through XML duration without issue.

@mvdan
Copy link
Member

mvdan commented Feb 11, 2025

We need to be able to encode sub-second durations, though. Milliseconds, microseconds, and even nanoseconds are generally useful, and they can be encoded with encoding/json v1 today, just like they can with https://pkg.go.dev/time#Duration.String.

@daenney
Copy link

daenney commented Feb 11, 2025

Agreed. And that's fine. All I'm asking for is to not allow the unnecessary trailing zeros. That would break compatibility with existing formats, like XML duration, and doesn't seem to provide any benefit.

@neild
Copy link
Contributor

neild commented Feb 11, 2025

JSON integer in nanoseconds (e.g., 47683228428000)
JSON fractional number in seconds (e.g., 47683.228428)

Both of these have the problem that it's too hard to see what the field contains and too easy to get the unit wrong.

JSON string using time.Duration.String (e.g., "13h14m43.228428s")

Too Go-specific. Trivial to parse for Go programs, but everyone else has to write a custom parser.

JSON string using ISO 8601 (e.g., "PT13H14M43.228428S")

At least it's kind of a standard? But if we need to write an Internet Draft to standardize it enough to be usable, it doesn't seem like a great choice.

JSON string using base60 representation (e.g., "13:14:43.228428")

Everyone has to write a custom parser, so at least it's ecumenical?

Of these options, attempting to set precedent for an ISO 8601 subset seems like the best. But I think they're all inferior to the very simple alternative used by the protobuf JSON encoding: Fractional seconds with an "s" suffix, e.g., "47683.228428s". This is trivial to parse, hard to misinterpret, and scales to whatever precision we want.

@dsnet
Copy link
Member Author

dsnet commented Feb 11, 2025

At least [ISO 8601 is] kind of a standard? But if we need to write an Internet Draft to standardize it enough to be usable ...

The grammar I proposed above is going to encode as something that is almost certainly compatible with any decoder that claims compliance with ISO 8601. The challenge is what to do when inevitable bugs get filed claiming that the encoded output of some non-Go system could not be handled by Go's decoder for ISO 8601. We could keep loosening the decoder to support whatever bugs are filed, but this process of not knowing what "compliance" means is problematic, but not fatal. By virtue of everyone functionally approaching ISO 8601 with Postel's law, the format has become fairly usable, but landmines exist if you encode something that strays from the core grammar.

One could imagine v2 using a restricted ISO 8601 profile, while also pursuing an RFC to codify that particular profile. While not unique to Go, the language has set the precedent of have strong specifications first and making the implementation follow the specification, rather than the other way around. We could be a trend setter and being the ones that advocate for a widely agreed duration format based on ISO 8601.

@neild
Copy link
Contributor

neild commented Feb 11, 2025

Even as a "simple alternative", we would still need a precisely described grammar to define what a valid format is.

Sure, but we can define a simple and precise grammar. I'm suggesting the general notion of "floating point seconds followed by the letter s", not precise compatibility with protobuf JSON implementations.

@timbray
Copy link

timbray commented Feb 11, 2025

I suppose you maybe have the best of both worlds with an absolutely brutally minimal subset of 8601, where all serializations have only seconds?

/^PT([1-9][0-9]*)|(0\.[0-9]*[1-9])|([1-9][0-9]*\.[0-9]*[1-9]))S$/

The regexp gibberish is trying to say that the numeric part can look like 3412 or 0.3412 or 34.12 but not 034.12 or 34.120

@timbray
Copy link

timbray commented Feb 11, 2025

Oh, pardon me, that doesn't work, apparently you can't have more than 60 seconds. sigh So probably something close to what @dsnet proposed above.

@dsnet
Copy link
Member Author

dsnet commented Feb 11, 2025

apparently you can't have more than 60 seconds

It's still unclear to me. There are several editions of ISO 8601. The 1988 edition mentions optional carry-over requirements, but at least one of the later editions does not seem to? Since ISO 8601 requires payment and has had 5 major editions, it's hard to check what's actually required or not.

Your proposed syntax of just PT123456.789S also sounds fine to me, but we probably want to test out the ISO 8601 implementation in a number of language.

@timbray
Copy link

timbray commented Feb 11, 2025

Also 3339 only allows 0-59.

@ianlancetaylor ianlancetaylor moved this to Incoming in Proposals Feb 12, 2025
@mpx
Copy link
Contributor

mpx commented Feb 13, 2025

I'm not convinced there will be any momentum behind using a more complete ISO8601 encoding for JSON durations across the broader JSON ecosystem (beyond Go). Eg, that would need libraries for other languages and a desire to use them. Otherwise anyone interfacing with Go JSON durations will have to built it themselves - lots of opportunity for bugs/frustration.

Making Go JSON durations more awkward to use/interoperate seems like a significant disadvantage. Standards date/time formats are great, but unfortunately I don't think we'll realise much broader benefit here.

This leads me back to fractional seconds + trailing "s". It's unambiguous when encountered in the wild, and trivial to parse/encode. Much more likely to work well in the broader JSON ecosystem for interoperability.

It's mostly the time.Duration.String option from the initial breakdown above, but not Go specific.

@daenney
Copy link

daenney commented Feb 13, 2025

The ISO-style duration format, PXXXXTYYYY is already well-supported:

  • MomentJS understands it
  • So does NodaTime for .NET/C#
  • JodaTime for Java
  • PHP's DateInterval can be constructed with it
  • ActiveSupport::Duration in Ruby/Rails can parse
    And more.

The Schema.org Duration format also uses this, which means it's fairly well recognised on the web and used in microformats already.

If we're talking interoperability with other platforms, that seems the format that provides the most convenient out of the box experience, as it wouldn't require any custom logic to parse a "1.0475s" string into something meaningful.

One potential issue though is that some of those parses might do the same thing as XML's duration, so they wouldn't recognise a duration with more than 23H, 59M and 60S, expecting anything over that to turn into P1DT instead. That would be more annoying.

@mpx
Copy link
Contributor

mpx commented Feb 13, 2025

Is there general agreement that ISO8601 durations across existing implementations that they must never use years/months/days/weeks?

I suspect any usage of ISO8601 will face an awkward tension between supporting more than hours/minutes/seconds since someone else uses it, or frustration from interoperability issues - looks like an ISO8601 duration, but it doesn't work.

Go's time.Duration is a real time interval, so years/months/days/weeks must not be used. Other platforms may encode civil time periods where years/months/days/weeks makes sense. This confusion seems unavoidable with ISO8601 periods/durations.

..which brings me back to fractional seconds + trailing "s" as less ambiguous and better for interoperability.

@neild
Copy link
Contributor

neild commented Feb 13, 2025

I'm fairly convinced now that ISO-8601 is not the right default.

ISO-8601 durations explicitly support time intervals. "P1Y" is one year, and a variable number of seconds depending on the starting time. "P10S" is ambiguous as to whether it means ten seconds in monotonic or civil time.

I just don't see any advantage to using ISO-8601 to marshal a time.Duration by default. Interoperability isn't a good argument if we're just making it easier for people to make mistakes. For example, @daenney points out that PHP's DateInterval can be constructed from an ISO-8601 duration string, but that type appears to be a civil time interval.

Fractional seconds and a suffix is unambiguous and trivial to parse, and anyone who wants to use a different encoding can easily plug in a type-specific marshaler.

@doggedOwl
Copy link

doggedOwl commented Feb 13, 2025

Interoperability isn't a good argument

This is the strangest thing to say when talking about an interoperability medium. Honestly sometimes it seems that go devs just live in a go walled garden. Interoparability is the main concern of JSON.
Even if the the go json library comes up with the "perfect solution for go", you are just pushing those mistakes that you say are easy to make to other parts of the stack because inevitabily that json will need to be parsed by something other than go, or go needs to consume json from somewhere else.

@dsnet
Copy link
Member Author

dsnet commented Feb 13, 2025

Honestly sometimes it seems that go devs just live in a go walled garden.

To be fair, I guess I'm technically a Go developer, and while I earlier proposed use of time.Duration.String as the default, I'm making an honest effort to see if we can make ISO 8601 work because interoperability of our "json" package with systems not written in Go is one of the goals of the v2 design. For example, it's for this reason why nil []T and map[K]V marshal (in v2) as [] or {} because that avoids leaking details about the Go type system to some external system that has a different language type system.

My main concern with "fractional seconds and a suffix" is that nothing else (to my knowledge) uses this format except ProtoJSON, which is arguably a small use case since most protobuf messages are serialized using the binary wire format.

OpenAPI, which is a relatively well-used system for JSON-based APIs uses ISO 8601 for time durations. It's on my TODO list to understand how it handles years, months, days, etc.

[ISO 8601 is] ambiguous as to whether it means ten seconds in monotonic or civil time.

ISO 8601 seems to distinguish between "accurate" versus "nominal" duration values. Thus, this split of treating hours, minutes, seconds differently is at least called out within the standard:

Duration can be expressed by a combination of components with accurate duration (hour, minute and
second) and components with nominal duration (year, month, week and day). The term duration will be
used to designate expressions containing components with accurate duration, with nominal duration,
or both.

I'm aware of at least some protocols that claim support for ISO 8601 and only accept hour, minute, and seconds.

@mpx
Copy link
Contributor

mpx commented Feb 14, 2025

ISO8601

OpenAPI defines durations as ISO8601, sourced from JSON schema:

The duration format is from the ISO 8601 ABNF as given in Appendix A of RFC 3339.

RFC3339 Appendix A includes support for civil time periods (years/months/days/week).

I think any use of ISO8601 would need a good answer for how to handle civil time periods. Unlike Proto, JSON doesn't have type information so the form of the value may be used to indicate the type of data (eg, ISO8601 => civil time periods).

Umarshalling into a time.Duration with a value matching ^P[^T] would need to throw an error (eg, "civil time period not supported by time.Duration"). I can't see attempting approximate time conversions would be acceptable. Developers would need to use a different type for that.

If Go gains a civil.Period type, then ISO8601 would be a reasonable encoding (and it would be more compatible) with ISO8601 generally.

Compatibility

For me this is less about being compatible with ProtoJSON, and more about providing clear intent when looking at the encoded JSON. From what I've seen, many different encodings are used across the JSON ecosystem - with numeric encodings being most common (floating point/integer seconds/milliseconds/microseconds), but these are all problematic since the type is unclear looking at the numeric value itself.

From a design POV, I think it would be clearer for real-time durations and civil time periods to be encoded differently. Fractional seconds + "s" is unambiguous and easier to encode/decode if you need to write it yourself.

However, if the ecosystem is actively moving towards ISO8601 for real-time durations despite the ambiguity, then maybe aiming for "future" compatibility and throwing an error is the best we can do.

ISO8601 durations seem problematic and it's not clear to me they are the "future" for JSON, so I'd lean towards the clearer encoding. That said, I can totally see people could come to a different conclusion about where this is heading.

@dsnet
Copy link
Member Author

dsnet commented Feb 14, 2025

OpenAPI's reference of RFC 3339, Appendix A is problematic because RFC 3339 itself disavows being an authoritative grammar by saying: "This is informational only and may contain errors." In fact, it does contain errors. The grammatical construction makes it such that PT1H1S is invalid since dur-hour can only be followed by an optional dur-minute (not a dur-second). However, the formatters that I tried (i.e., Java and Python) will actually output PT1H1S over PT1H0M1S.

For me this is less about being compatible with ProtoJSON, and more about providing clear intent when looking at the encoded JSON.

I believe this is where we're fundamentally in disagreement (which is fine). I believe that compatibility with the wider ecosystem is more important than clarity. Clarity is a good goal (and it's entirely reasonable for others to hold to this), but at least for JSON, I believe interoperability is more important since JSON has become the de-facto langua fraca of how many machines or programs communicate with each other. Also, ISO 8601 isn't notably "unclear" relative to "fractional seconds and a suffix". For example, if I see "PT1M", it's guessable that it might mean "1 minute". Asking ChatGPT, Gemini, or Grok what "PT1M" means all results in them identifying it correctly as ISO 8601.

(To be honest, I like the simplicity and aesthetics of Go's time.Duration.String or "fractional seconds and a suffix", but I don't want to be guilty of adding yet another format that is different most other ways).

However, if the ecosystem is actively moving towards ISO8601 for real-time durations despite the ambiguity

I may be wrong, but I've been seeing increasingly more evidence of this being functionally true (e.g., Java's time.Duration uses ISO 8601 for the toString method). Supposing that ISO 8601 is the de-facto standard, then one proposal is for "json/v2" to use a strict subset of ISO 8601 and pursue a RFC standard (like RFC 3339) that codifies a concise and limited grammar for a duration. The strict subset has no accuracy problems and some formatters (e.g., Java) already output exactly that format. Unfortunately, Python formats large durations using days (e.g., P1DT10H17M36.123456S), which ISO 8601 calls out as being "nominal" rather than "accurate". Fortunately, Java does the right thing and uses larger hour values (e.g., PT34H17M36.123456S).

Let's suppose an RFC one day exists that codifies a concise grammar. I can see a path forward where the Internet can actually stabilize on an interoperable format. I suspect:

  • Practically all ISO 8601 parsers will already be compliant.
  • Many ISO 8601 formatters will already be compliant.
  • For formatters that are not compliant, it is arguably backwards compatible to output the grammar specified in the RFC.
  • For many pre-existing system already on ISO 8601, it does mean that they can parse inputs wider than what is allowed by the RFC, but that's okay. If you want interoperability, use the RFC, otherwise it might work or might not (just like how it already is today).

@puellanivis
Copy link

I think the ISO-8601 definition is sufficiently clear enough to settle production: the time.Duration should only represent at most hours, minutes, and seconds (with fraction). I don’t think the difference between 1234.123456789s and PT1234.123456789S is particularly great enough to be anything other than bike-shedding, and ISO-8601 seems to suggest that this may appropriately be considered an accurate time, period and thus appropriate for time.Duration.

This really only leaves a problem when we are decoded a time.Duration and we receive a nominal date period. This sort of data would be outside of the capability for time.Duration to represent. A time.NominalPeriod or time.CivilPeriod would be useful in this regard, but it would end up needing to be essentially a separate field for each unit. (P1DT48H being an obviously oddball hybrid period. P1W2D also being fun when it’s skipping from 1752-9-1 to 1752-9-21.) Point is, this nominal/civil time period support is patently out of scope for the time.Duration type… and we could thus appropriately reject any nominal/civil time period elements as unsupported, or invalid.

Anyone who needs to support such time periods can be left to implement their own… or submit their own proposal to provide a solution to the standard library. (I recall that I’ve had to support this before.)

@dsnet
Copy link
Member Author

dsnet commented Feb 14, 2025

Slightly off topic, but given the low voter turnout for the "base60" format (e.g., "13:14:43.228428"), I propose removing support for it from the v2 proposal.

👍 if you agree (remove it from v2), 👎 if you disagree (keep it in v2).

@mvdan
Copy link
Member

mvdan commented Feb 14, 2025

SGTM with the assumption that we can always add base60 as an opt-in format later.

@daenney
Copy link

daenney commented Feb 15, 2025

XML duration turns out to actually allow fractional seconds, so PT1.693S is fine. Meaning my comment to not allow unnecessary trailing zeroes, like PT1.000S can be ignored. The spec presents the regex ([0-9]+(\.[0-9]+)?S) for the seconds definition.

From a cursory glance at some parsers, it seems some would accept a value like PT370.68S, which would allow time.Duration to be correctly expressed in "elapsed seconds" in all cases. I haven't yet tested this.

That may allow the use of the ISO-style format for serialisation, without the perils of civil time, if Go were to constraint the serialisation of time.Duration to always be PTXXX.YYYYS. But it does create a question as to what happens when we receive JSON with a duration that does use a civil time component.

@willfaught
Copy link
Contributor

willfaught commented Feb 17, 2025

Note that time.Duration.String returns "µs" instead of "us" (which time.ParseDuration accepts), which might be annoying (or impossible) to handle manually in another language. It would be nice to always produce "us" instead.

@puellanivis
Copy link

Note that time.Duration.String returns "µs" instead of "us" (which time.ParseDuration accepts), which might be annoying (or impossible) to handle manually in another language. It would be nice to always produce "us" instead.

I think the current intent is that we never generate either µs or us, but rather 0.0000001s or PT0.000001S

@dsnet
Copy link
Member Author

dsnet commented Feb 20, 2025

The debate regarding using time.Duration.String versus "fractional seconds with a suffix" versus a subset of ISO 8601 has largely stalled because none of us are really know how prevalent ISO 8601 truly is. Currently, there are more upvotes for time.Duration.String, but I suspect there is some bias since this issue is filed on the Go issue tracker. Furthermore, we never gave "fractional seconds with a suffix" a fair chance since it wasn't part of my initial voting post.

To help resolve this, we (with the help of @timbray) wrote a RFC Internet-Draft to standardize on a concise grammar for durations based on ISO 8601 that we can circulate more widely outside the Go community. Regardless of where that effort goes, the wider reaction to the RFC draft will be helpful:

  • If the wider industry supports the idea of moving towards standardization, then that's a strong signal that a strict profile of ISO 8601 should be the default. So long as we're confident that the proposed grammar will not change, we can move forward with "encoding/json/v2" without waiting for the RFC to become an actual standard.

  • If the wider industry is opposed to the idea, then that's a strong signal that we should pursue some other default such as just using time.Duration.String or "fractional seconds with a suffix".

  • If the wider industry is ambivalent... well that would be highly unfortunate as it gives us little signal on what to do.

@timbray
Copy link

timbray commented Feb 21, 2025

I would add that there is also a much prettier HTML version of the Internet-Draft at https://www.ietf.org/archive/id/draft-tsai-duration-00.html

dsnet added a commit to go-json-experiment/json that referenced this issue Feb 22, 2025
By popular demand (or rather lack thereof), remove support
for the base60 representation of a time duration.
There is no standard for this format and even the name is an attempt
at naming something without clear industry basis for what this is called.

Updates golang/go#71631
@puellanivis
Copy link

Great RFC; two notes:

dur-secfrac = period 0*digit0-9 digit1-9

Typo? It seems there’s an extraneous 0 before *digit0-9. (I’m sure everyone can figure it out, but it’s in a notoriously “pedantic” region of an RFC.)

This example could instead be expressed as PT10272H assuming that the accurate duration is calculated relative to January 1st, 2000 based on the Gregorian calendar.

😂 Love this example, because it depends on 2000 being a leap year in the Gregorian calendar, which was a common Y2K bug that often made it into the year 2000. I also just had to go double-check, and from the sources I found, I can confirm there were no leap seconds in that interval.

@dsnet

This comment has been minimized.

dsnet added a commit to go-json-experiment/json that referenced this issue Feb 23, 2025
By popular demand (or rather lack thereof), remove support
for the base60 representation of a time duration.
There is no standard for this format and even the name is an attempt
at naming something without clear industry basis for what this is called.

Updates golang/go#71631
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LibraryProposal Issues describing a requested change to the Go standard library or x/ libraries, but not to a tool Proposal
Projects
Status: Incoming
Development

No branches or pull requests