If you’ve written Go long enough, you’ve bumped into the rough edges of encoding/json. Maybe it was the silent swallowing of unknown fields, or the way it handles duplicate keys, or some marshaling behavior that didn’t quite match what the JSON spec says it should do. The package works — well enough that it’s the 5th most imported in the entire Go ecosystem — but “works well enough” has always felt like damning with faint praise for something so foundational.
Go 1.25 takes a real swing at this with the new experimental encoding/json/v2 and encoding/json/jsontext packages.
Why v2 Instead of Just Fixing v1?
This is the right question to ask. Go takes API stability seriously — the compatibility promise is a core part of why large codebases trust the language. But that same promise means encoding/json has been carrying behavioral baggage for years that can’t be fixed without breaking existing code.
The Go team’s answer is to ship a new major version alongside the old one, not replace it. encoding/json stays exactly as it is. encoding/json/v2 is the clean slate.
The issues being addressed fall into a few buckets:
- Imprecise JSON syntax handling — the original package has drifted from the spec in subtle ways over the years, as JSON itself has seen increased standardization
- Behavioral surprises in marshaling and unmarshaling that accumulated as edge cases nobody wanted to break by fixing
- Missing features that users have been papering over with third-party packages for years
What the New Packages Look Like
There are two packages in play here. encoding/json/jsontext handles the low-level token-by-token reading and writing of JSON — think of it as the lexer layer. encoding/json/v2 builds on top of that with the higher-level marshal/unmarshal API you’re used to.
This layering is smart. It means you can drop down to the tokenizer when you need precise control, without reimplementing everything yourself.
The familiar patterns still work:
data, err := json.Marshal(v)
err = json.Unmarshal(data, &v)
But the semantics are tighter, the spec compliance is stricter, and the customization points are cleaner.
The Experimental Caveat
These packages are not visible by default and the API may still change. This is the Go team being appropriately cautious about a package that will eventually need to be stable forever. The right move is to try it in non-production code, file issues where the behavior surprises you, and let the feedback loop do its job.
The fact that it’s experimental also means now is the best time to influence the final API. If there’s a design decision you think is wrong, the window to say so is open.
My Take
I’ve reached for third-party JSON packages in Go more times than I’d like to admit — mostly for features that really should have been in the standard library. A well-designed v2 that actually ships as part of Go would remove a whole category of dependency decisions.
The approach of running v1 and v2 in parallel indefinitely is the right call. There’s too much existing Go code for a hard cutover to be practical. What matters is that new projects can start clean.
This has been a long time coming. Fifteen years is a long run for any API without a ground-up rethink.