From Discussion to Proposal
Originally published in Japanese at https://zenn.dev/ymotongpoo/books/go-json-v2-history/viewer/40-proposal.
The precedent of math/rand/v2
On October 5, 2023, Joe Tsai opened GitHub Discussion #63397 (“encoding/json/v2”). Almost three years had passed since the prototype’s first commit.
Why that timing? Two days earlier, on October 3, the math/rand/v2 proposal golang/go#61716 had been accepted.
Russ Cox, who filed the math/rand/v2 proposal, had opened Discussion #60751 that June under the title “math/rand/v2: a new API for math/rand and a first v2 for std"—positioning it himself as the first v2 in the standard library.
Until then, even the question of whether a v2 belonged in the standard library at all had been unsettled. Two days after that was confirmed to be possible, the encoding/json/v2 discussion began.
What the points of contention were
Discussion #63397 drew many comments and thumbs-up reactions. The text explicitly states that it was written incorporating input from mvdan, johanbrandhorst, rogpeppe, chrishines, and rsc.
The main points of contention were these four.
| Point | v1 | v2 | Discussion |
|---|---|---|---|
| Map output order | Deterministic order with sorted keys | Non-deterministic order | Many objected from the standpoint of testing and easy diffing. The decision was not to impose the cost of sorting on everyone by default; those who need it specify the Deterministic option |
| Nil slices and maps | null | [] and {} | Objections were raised that round-tripping breaks and that the information of Go’s nil is lost |
The criterion for omitempty | Whether the value is empty as a Go value (false, 0, nil pointer, empty string, etc.) | Whether it would be an empty JSON value | The criterion moved from Go’s type system to JSON’s type system |
Putting null into a non-nullable Go type | Not an error | Not an error | Some argued it should be rejected, but this was dismissed on the grounds that valid JSON should not be rejected for the convenience of Go’s type system |
Permanent support for v1
A concern raised repeatedly in the discussion was that v1 might be deprecated.
The answer was unambiguous. The current encoding/json documentation includes this sentence.
All new usages of “json” in Go should use the v2 package, but the v1 package will forever remain supported.
In the same October 2023, Joe Tsai gave a talk at GopherCon 2023 titled “The Future of JSON in Go”—the same week the Discussion was opened1.
The proposal and a feature freeze
It took a year and three months to go from discussion to proposal. On January 31, 2025, golang/go#71497 was filed—a two-package proposal covering encoding/json/v2 and encoding/json/jsontext. In the proposal text, Joe Tsai calls it “the largest major revision of a standard Go package to date.”
Between the discussion and the proposal, several names changed. The prototype’s MarshalWriter / MarshalNext became MarshalWrite / MarshalEncode, and the Discussion-stage MarshalerV2 / UnmarshalerV2 became MarshalerTo / UnmarshalerFrom.
As review progressed, a no-new-features stance was made explicit. From Damien Neil’s comment of April 10, 2026:
We’re trying to get the already huge existing proposal over the line, and really want to avoid additional feature creep at this point. We’re much more likely to temporarily withdraw features from the v2 for the initial release so we can consider them in isolation than we are to add new ones.
That this did not end as a mere statement of posture became clear just before the release.
Trial availability via GOEXPERIMENT
In August 2025, the year the proposal was filed, Go 1.25 shipped v2 behind GOEXPERIMENT=jsonv2. Only when you build with the environment variable set are encoding/json/v2 and encoding/json/jsontext available. Without it, they are not included.
On September 9, 2025, the official Go blog published “A new experimental Go API for JSON”.
[The effort] has been largely developed and promoted by people not employed by Google, demonstrating that the Go project is a collaborative endeavor.
On November 20, 2025, a json/v2 working group was formed, and weekly meetings with public minutes began.
Settling time.Duration
One of the things decided in this period was the handling of time.Duration. The project’s whole way of thinking is condensed in it.
v1 emits time.Duration as an integer number of nanoseconds. golang/go#71631 was a sub-proposal to decide what v2 should do.
On December 11, 2025, following discussion in the working group, Damien Neil wrote the conclusion.
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.
If the JSON is consumed by JavaScript, any duration beyond 104 days loses precision. So what should it change to?
The world seems to be converging on ISO 8601 … 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.
And the conclusion reads:
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.
A new proposal emerged. In v2, calling Marshal on a time.Duration as-is is an error. It succeeds only when a format is specified explicitly. Neither “inherit the wrong default” nor “silently switch to the right default,” but “have no default, and make the writer choose.”
Acceptance of the proposal
On April 16, 2026, proposal #71497 was moved to active2.
On April 29, Austin Clements set a deadline: for it to be reviewed before the freeze, an updated version reflecting the changes since the 1.26 GOEXPERIMENT release was needed within one week.
The next day, April 30, Joe Tsai updated the proposal, noting that this was intended as the initial stable release and tentatively targeted Go 1.27.
And on the same day, by Joe Tsai’s own hand, one feature was withdrawn.
On May 6, the proposal review was held. The minutes survive, and they summarize the accepted design well.
@dsnet joined us for a walk through of the whole API. jsontext — Leans into performance over absolute safety. API tends to prefer aliasing over allocating. The intent is that this package isn’t widely used, and is only for doing really custom things and really high-performance things. Options — Shared across encoding and decoding and between the syntax and semantic layers. The WG spent a lot of time exploring alternatives, and ultimately came back to this design. json/v2 — UnmarshalRead will always read to EOF. This is in contrast to v1, where it was a common mistake to call
Decode(io.Reader)and not check that the reader had reached EOF. Struct tags —omitemptyis now defined in terms of the JSON type system instead of the Go type system. 👍 all around the room.
It was marked “likely accept” the same day and accepted on May 13, 2026. Austin Clements’s comment was short.
No change in consensus, so accepted. 🎉
On May 22 the milestone was set to Go 1.27, and on June 9 the issue was closed as implemented.
Ten years and two months after the issue asking “isn’t this wrong?” was filed in March 2016.
It is customary at GopherCon for a Contributor Summit—a gathering of the Go core team and developers making significant contributions to Go—to be held the day before the conference. The author suspects that offline conversations took place there. (Russ Cox’s Contributor Summit announcement) ↩︎
A Go proposal is filed as an issue and moves across the proposal review board: Incoming → Active → Likely Accept → Accepted. Right after filing it sits in Incoming, moving to Active when the reviewers have capacity. Active proposals are handled at the weekly proposal review meeting, with minutes posted to https://go.dev/s/proposal-minutes. Once consensus appears to have formed, a proposal moves to Likely Accept, and if no discussion overturning that consensus emerges within a week, it becomes Accepted (the rejection track is Likely Decline → Declined; proposals awaiting design revisions and the like go to Hold). In other words, this date is not the day acceptance was decided but the day the proposal landed on the review table. ↩︎