The Ecosystem After v2

Originally published in Japanese at https://zenn.dev/ymotongpoo/books/go-json-v2-history/viewer/70-after_v2.

The withdrawn format tag

On April 30, 2026 — the same day he updated the proposal — Joe Tsai opened one more issue: golang/go#79071, titled “encoding/json/v2: remove format tag option”.

The format tag was a mechanism for specifying a representation per field: put format:RFC1123 on a time.Time to choose the layout, format:base64url on a []byte to choose the encoding, and format:iso8601 or format:units on a time.Duration. The conclusion of the time.Duration debate — set no default and make the writer choose a format — was premised on this mechanism.

The stated reason for withdrawing the format tag:

With Go 1.28 prospectively having typed struct tags in some form or another, the json/v2 working group decided to remove support for the format tag option since this would be more naturally expressed as a typed struct tag.

The decision, in other words, is to wait for a language feature: typed struct tags.

Typed struct tags were proposed by Axel Wagner in July 2025 in golang/go#74472, a language change that adds typed constant expressions alongside today’s string tags. It would let you write time.Time `json:",format:RFC3339"` as something like time.Time {json.Format(time.RFC3339)}. Instead of building a small ad-hoc language inside a string, the direction is to let the compiler check it.

That proposal was put on hold in November 2025 as worth investigating but lacking the bandwidth to pursue right now. Then, on April 20, 2026, Austin Clements wrote:

I’ve started meeting with @rsc, @griesemer, @adonovan, and @neild to ‘jump start’ and prioritize this proposal. Partly this is because the likelihood and timeline of this proposal affects decisions on json/v2.

Ten days later, #79071 was opened and the format tag was withdrawn. An undecided proposal on the language side acted on the standard library proposal by removing one feature from it.

The Damien Neil comment quoted in chapter 4 — that temporarily withdrawing features from the initial release was far more likely than adding new ones — turned out not to be a figure of speech.

The problem left behind for time.Duration

Something strange happens here.

The resolution for time.Duration was to make the format explicit. The means of making it explicit was the format tag. And the format tag was withdrawn.

Try it and this is what you get:

type Config struct {
	Timeout time.Duration `json:"timeout"`
}

type ConfigTagged struct {
	Timeout time.Duration `json:"timeout,format:units"`
}
v1             : {"timeout":90000000000}  err=<nil>
v2             : {"timeout"  err=json: cannot marshal from Go time.Duration within "/timeout": no default representation
v2 format:units:   err=json: cannot marshal from Go main.ConfigTagged: Go struct field Timeout has unsupported `format` tag option

Without the tag: an error. With the tag: also an error. Inside the Go 1.27 standard library, there is no way to serialize a time.Duration with v2. You either implement Marshaler yourself or use v1.

Recall the answer Russ Cox gave when he closed #4712 in 2017: implement Marshaler yourself. For time.Duration, nine years of work led back to the same place.

The implementation itself remains. arshal_time.go still contains, intact, the code that interprets units, sec, milli, micro, nano, and iso8601; only the switch that enables it is hidden away in an internal package.

What exposes that switch is github.com/go-json-experiment/json. A comment on the standard library side says:

NOTE: While [ExperimentalSupportFormatTag] is exported, it is in an internal package and thus inaccessible for public use. The [github.com/go-json-experiment/json] module is kept in sync with the Go standard standard library and will expose this option in a way that public code can now directly reference.

The structure has come full circle. Because the standard library couldn’t move, a prototype was built outside it; the prototype was absorbed into the standard; and now, to touch a feature removed from the standard, you end up using that same external module again.

Note that neither the withdrawal of the format tag nor the fact that time.Duration now errors appears in the Go 1.27 release notes. The only place the release notes address encoding/json itself is the paragraph quoted in chapter 5 — “now backed by the v2 implementation” — in the “New encoding/json/v2 and encoding/json/jsontext packages” section.

Third-party responses

encoding/json moving onto v2 is a change of premise for the libraries that have sold themselves as drop-in replacements. The reactions split.

The one that engaged most seriously is bytedance/sonic. The pull request “feat: support Go 1.27” brought in more than 2,500 lines of changes. Out of it came a compatibility matrix, kept in the repository as docs/sonic-go127-compatibility.md.

The matrix compares three things: sonic’s own standard-compatible mode, Go 1.27’s v1 (the one now sitting on v2), and the old v1 you get back with GOEXPERIMENT=nojsonv2. Adding direct v2 to those, it records the differences among the four.

CI now runs Go 1.27 both with the default and with nojsonv2. To keep the promise of returning the same results as encoding/json, there are more targets to chase.

Some of the differences the matrix records:

  • When reading a number that exceeds the range of float64, Go 1.27 returns an error and sets the destination to +Inf, while sonic returns an error and leaves the destination at 0.
  • sonic rejects map[float64]string, but Go 1.27 can encode it.
  • For a type that has both AppendText and MarshalText, Go 1.27 prefers AppendText.

For goccy/go-json, I could not find any reaction to v2 on GitHub. There is no mention in the issues or the README, and the roadmap text is unchanged1.

json-iterator/go has been archived. Last updated in May 2024, the repository is now read-only. The most-starred drop-in third-party replacement has stopped, and its role has moved into the standard library. For eight years, this library carried side by side a README claiming a “100% compatible drop-in replacement” and an open issue titled “not 100% compatible drop-in replacement”.

Where protojson and jsontext stand today

What gave birth to jsontext was protojson, which couldn’t use encoding/json. So is protojson using jsontext now?

I checked, and it isn’t yet.

golang/protobuf#1673 is an issue asking to let protojson accept an io.Writer, and it remains open. In January 2025, Joe Tsai wrote a design there:

func MarshalWrite(io.Writer, proto.Message) error
func MarshalEncode(*jsontext.Encoder, proto.Message) error
func UnmarshalRead(io.Reader, proto.Message) error
func UnmarshalDecode(*jsontext.Decoder, proto.Message) error

And he writes:

The entirety of the internal/encoding/json package can be removed and replaced with the prospective encoding/json/jsontext package. In fact, internal/encoding/json can be thought of as an early prototype for what eventually became jsontext.

Once the switch happens, you could also write things like this:

json.Marshal(v,
    json.WithMarshalers(json.MarshalToFunc(protojson.MarshalEncode)),
)

That is, serializing a large Go value containing proto.Message fields in a single pass, while delegating just those parts to protojson.

Michael Stapelberg2, a Go Protobuf maintainer, chose at the time to wait for it to land in the standard library, on the grounds that he didn’t want to add dependencies and vendoring is a hassle. His comment of July 20, 2026 reads: “json/v2 is being released this month. I think the decision to wait has paid off.”

As of now, however, the protobuf-go source contains no reference to jsontext. The hand-written internal/encoding/json also remains. Go Protobuf’s policy is to support several older Go versions, so the actual switchover is still some way off.

Joe, who gave birth to jsontext, is now waiting for the moment he can use it.


  1. There appears to be no mention on X either↩︎

  2. Michael Stapelberg is known for releasing Go packages for Debian (apt packaging) and for developing i3wm. ↩︎