# The Ecosystem After v2

> Source: https://www.ymotongpoo.com/books/go-json-v2-history/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`](https://github.com/golang/go/issues/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 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`](https://github.com/golang/go/issues/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](https://github.com/golang/go/issues/74472#issuecomment-4284572415):

> 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 unsettled proposal on the language side ended up removing one feature from the standard library proposal.

The [Damien Neil comment](https://github.com/golang/go/issues/71497#issuecomment-4224664008) 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:

```go
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`](https://github.com/golang/go/issues/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](https://go-review.googlesource.com/c/go/+/788420) is hidden away in an internal package.

What exposes that switch is [`github.com/go-json-experiment/json`](https://pkg.go.dev/github.com/go-json-experiment/json#ExperimentalSupportFormatTag). 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](https://go.dev/doc/go1.27). 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](https://go.dev/doc/go1.27#jsonv2).

## 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`](https://github.com/bytedance/sonic). The pull request ["feat: support Go 1.27"](https://github.com/bytedance/sonic/pull/957) brought in more than 2,500 lines of changes. Out of it came a [compatibility matrix](https://github.com/bytedance/sonic/blob/main/docs/sonic-go127-compatibility.md), 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`. It adds direct v2 use to those three and records the differences across all four.

CI now runs Go 1.27 both with the default and with `nojsonv2`. To keep its promise of matching `encoding/json`'s results, sonic now has 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`](https://github.com/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 unchanged[^x-goccy54].

[^x-goccy54]: There [appears to be no mention on X either](https://x.com/search?q=from%3A%40goccy54%20json%2Fv2%20until%3A2026-08-20&src=typed_query&f=live).

[`json-iterator/go`](https://github.com/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 gone quiet, and its role has moved into the standard library. For eight years, this library ran a README claiming to be a "100% compatible drop-in replacement" right alongside an [open issue titled "not 100% compatible drop-in replacement"](https://github.com/json-iterator/go/issues/229).

## Where protojson and jsontext stand today

What gave birth to `jsontext` was [`protojson`](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), which couldn't use `encoding/json`. So is `protojson` using `jsontext` now?

I checked, and it isn't yet.

[`golang/protobuf#1673`](https://github.com/golang/protobuf/issues/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](https://github.com/golang/protobuf/issues/1673#issuecomment-2605580160):

```go
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:

```go
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 Stapelberg[^stapelberg], 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](https://github.com/golang/protobuf/issues/1673#issuecomment-5020103523) reads: "json/v2 is being released this month. I think the decision to wait has paid off."

[^stapelberg]: Michael Stapelberg is known for releasing Go packages for Debian (apt packaging) and for developing i3wm.

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.

