Go
The Evolution of Go JSON: Exploring the Experimental v2 API in Go 1.25
Published:
•
Duration: 7:19
0:00
0:00
Transcript
Host: Hey everyone, welcome back to Allur! I’m your host, Alex Chan. Today, we are diving deep into a topic that is quite literally the bread and butter of almost every Go developer out there: JSON. Now, if you’ve been writing Go for any amount of time, you know that `encoding/json` is probably the most used package in the standard library. It’s been our reliable, if somewhat quirky, companion for over a decade. But as the world of web services has grown more complex, some of the limitations of that original v1 design have really started to show their age. We’re talking about heavy reflection, those weird global state issues, and the constant battle with `omitempty`. Well, the wait for a modern solution is finally over—sort of! With Go 1.25, we’re seeing the experimental release of the JSON v2 API. It’s a foundational redesign that promises better performance and way more control. Today, we’re going to look at what’s changing, why it’s split into two packages, and why you might finally be able to stop using pointers just to handle optional integers. It’s a big one, so stay tuned!
Host: To help me break all of this down, I am joined today by Marcus Thorne. Marcus is a principal engineer who’s spent the last seven years building high-scale microservices in Go, and he’s been closely following the proposal for JSON v2 since its inception. Marcus, it’s so great to have you on Allur!
Guest: Thanks, Alex! It’s great to be here. Honestly, I’ve been refreshing the Go dev blogs waiting for this experimental release for months, so I’m really excited to chat about it.
Host: I bet! I mean, JSON is everywhere in Go. But before we get into the shiny new stuff in Go 1.25, let’s talk about the "why." Why did the Go team decide that a simple patch wasn't enough? Why do we need a whole new "v2" for something as fundamental as JSON?
Guest: Yeah, that’s the million-dollar question. I think it comes down to the fact that the original `encoding/json` was designed in a different era of Go. Back then, reflection was the easiest way to bridge Go structs and JSON text. But as our services started handling millions of requests per second, that reflection became a massive performance bottleneck. And then there are the "quirks"—like, if you wanted to disable HTML escaping, you had to change global state or use a custom encoder, which is just... um, it's not very "Go-like" in terms of safety and predictability. The team realized that to truly fix the performance and the ergonomics, they needed to break some things, and they couldn’t do that within the stability guarantees of the v1 package.
Host: Right, the Go 1 compatibility promise is a double-edged sword sometimes! So, in Go 1.25, they’ve introduced these two new experimental packages: `encoding/json/jsontext` and `encoding/json/v2`. That split seems really intentional. What’s the logic behind having two separate packages?
Guest: It’s actually my favorite part of the redesign. They’ve essentially decoupled the "how" from the "what." The `jsontext` package is what I call the "engine room." It’s a low-level API that deals strictly with tokens—you know, your delimiters, strings, numbers, and bools. If you’re building a high-performance proxy or a middleware where you don't actually need to unmarshal a whole object into a struct, you just use `jsontext`. It’s incredibly fast because it doesn't do the heavy lifting of mapping to Go types.
Host: Oh, interesting! So you could essentially "peek" at a JSON key in a stream without paying the "tax" of unmarshaling the entire payload?
Guest: Exactly! You just read the tokens you need. And then, sitting on top of that, you have `encoding/json/v2`. That’s the high-level API most of us will use. It handles the mapping to structs, but it uses the optimized engine of `jsontext` under the hood. It’s much cleaner. No more global variables for configuration—everything is passed as options at the call site.
Host: That sounds way more flexible. Now, I have to ask about the one feature I know everyone is talking about: `omitzero`. I can’t tell you how many times I’ve had to explain to a junior dev why their "0" value disappeared from their JSON because they used `omitempty`.
Guest: [Laughs] Oh, the `omitempty` struggle is real! We’ve all been there where we end up using `*int` or `*bool` just to differentiate between "this field is missing" and "this field is zero." It makes the code so messy with nil checks everywhere. `omitzero` is the hero we’ve been waiting for. It literally just checks if the value is the "zero value" for that type. So, if you have an integer and it's 0, it knows it's the zero value. It’s just much more semantically clear than the old "is it empty?" logic which was always a bit... actually, it was very ambiguous.
Host: Yeah, "ambiguous" is a polite way to put it! Another thing I noticed in the proposal was a move toward more "strictness." V1 was pretty forgiving—maybe too forgiving?
Guest: Definitely. V1 would do case-insensitive matching by default, and it would silently ignore fields in the JSON that weren't in your struct. That leads to some really nasty bugs where you think you're capturing data, but you're actually dropping it because of a typo. In v2, you can actually opt-in to strictness. You can say, "Hey, if there’s an unknown field in this JSON, throw an error." Or, "I want case-sensitive matching only." It forces your data contracts to be much more explicit.
Host: That feels like it would save a lot of debugging time during integrations. But Marcus, what about performance? You mentioned reflection being the old bottleneck. Does v2 actually move the needle there?
Guest: It really does. Even though it’s still in the `exp` or experimental tier, the early benchmarks are showing significant reductions in allocations. The Go team has leveraged a decade of compiler improvements and better escape analysis. Plus, the new interfaces for custom marshaling—the `jsonv2.Marshaler`—provide better buffer management. Instead of creating a bunch of temporary strings or bytes, it can write more directly to the stream.
Host: That’s a massive win for high-traffic apps. Now, let’s talk about the "experimental" part. This is in Go 1.25 as `encoding/json/v2` under the experimental flag. Does that mean we shouldn't use it in production yet?
Guest: I’d be cautious. The "experimental" tag means the API could still change based on community feedback. The Go team is actually asking us to break it! They want to know if the strictness is too annoying in real-world scenarios or if there are edge cases with the new `jsontext` streaming. But, for internal tools or non-critical paths? I’d say start playing with it now. The interoperability is actually great—v2 can still call your old v1 `MarshalJSON` methods, so you don't have to rewrite your entire codebase at once.
Host: Oh, that’s a relief! I was worried about a "Python 2 to 3" situation where the whole ecosystem splits.
Guest: [Laughs] No, the Go team is way too obsessed with compatibility for that. They’ve built a really clever bridge. You can migrate one struct at a time, see how it feels, and move forward.
Host: So, if someone is listening and wants to give this a spin today, what’s the best way to get started?
Guest: Honestly, just pull down the Go 1.25 toolchain, and look for the `jsontext` and `v2` packages. Try replacing a few `json.Marshal` calls with `jsonv2.Marshal` and see what happens. Especially look at your struct tags—try out `omitzero` and see if you can get rid of some of those pointer-based hacks.
Host: I think "getting rid of pointer-based hacks" might be the most exciting sentence I’ve heard all week. Marcus, this has been so enlightening. It’s great to see Go evolving while still keeping that focus on simplicity and performance.
Guest: Totally agree. It’s a huge step forward for the language.
Host: That was Marcus Thorne, talking us through the evolution of JSON in Go 1.25. It really feels like the team has listened to years of community feedback to fix those little friction points we’ve all just "learned to live with." From the split between low-level `jsontext` and high-level `v2` to the long-awaited `omitzero` tag, there is a lot to be excited about. If you’re working on a Go project, I highly recommend checking out the Go Blog's post on "JSON v2 Experimental" to see the full technical breakdown.
Tags
Go
Golang
Coding
backend
performance
modernization
json