The Go team announced the new JSON API on its blog on November 4, 2026, in a post by Michael Pratt, Joe Tsai, Daniel Martí, Johan Brandhorst-Satzkorn, Roger Peppe, Chris Hines and Damien Neil. encoding/json/v2 and encoding/json/jsontext are stable and ship in Go 1.27. Go 1.25 shipped them as an experiment, and the effort as a whole took about six years.
You don't have to migrate. Both packages fall under the Go 1 compatibility promise, and the team says v1 users "need not ever migrate to v2." The reasons to move anyway are safer defaults and faster decoding.
Two packages
jsontext handles syntax: tokens, values, streaming reads and writes. It doesn't use reflection. json/v2 handles semantics, mapping JSON values to Go values on top of jsontext.
import (
"encoding/json/jsontext"
json "encoding/json/v2"
)
// Read straight from an io.Reader. No Decoder needed.
var cfg Config
if err := json.UnmarshalRead(r, &cfg); err != nil {
return err
}
The v2 API pairs each operation with three entry points: Marshal/Unmarshal for byte slices, MarshalWrite/UnmarshalRead for io.Writer and io.Reader, and MarshalEncode/UnmarshalDecode for jsontext encoders and decoders. Every function accepts options as variadic arguments, a gap in v1, where options lived on Encoder and Decoder and got lost when a custom UnmarshalJSON ran.
v1's RawMessage is now a type alias for jsontext.Value.
Default behavior changes
| Input or value | v1 | v2 |
|---|---|---|
| Invalid UTF-8 | Accepted | Error |
| Duplicate object member names | Accepted | Error |
| Nil slice or map | null | [] or {} |
| Struct field matching on unmarshal | Case-insensitive | Case-sensitive |
Two more changes sit outside the table. v2 redefines omitempty to omit a field that would encode as an empty JSON value: null, "", [] or {}. It also returns an error when you serialize a time.Duration, since the type has no default JSON representation, and gives you options to choose one. Most of these behaviors have a struct tag option or a caller option that restores v1 semantics.
The duplicate-name rule closes a known attack. RFC 8259 leaves duplicate names undefined, so two parsers can read the same object two ways, and the post cites CVE-2017-12635 as a past exploit of that gap. If a gateway in your stack validates a JSON body and a Go service acts on it, v2 now rejects the ambiguous version up front.
The nil-slice change will show up in API responses. A handler that returned "items": null for a nil slice returns "items": [] under v2. The Go team cites a survey in which most Go users preferred the empty array, and other JSON implementations often refuse null where they expect an array. A client that checks === null will notice the switch.
Faster decoding
The post puts v2 Marshal speed about level with v1, sometimes a bit faster and sometimes a bit slower. Unmarshal runs up to 10x faster in the team's benchmarks. The larger gains come from two new interfaces:
type MarshalerTo interface {
MarshalJSONTo(*jsontext.Encoder) error
}
type UnmarshalerFrom interface {
UnmarshalJSONFrom(*jsontext.Decoder) error
}
v1's UnmarshalJSON forces the package to find the end of a value before calling you, and then you parse it again. Recursive calls make that quadratic. Kubernetes hit it in kube-openapi, and switching to UnmarshalJSONFrom sped up OpenAPI parsing by orders of magnitude. If you maintain a type with a hand-written UnmarshalJSON on a hot path, add UnmarshalJSONFrom next to it.
The team plans more performance work for the cases where v2 trails v1, with improvements expected in Go 1.28.
Mixing versions
The Go team rebuilt v1 on top of v2. v1 is v2 with a full set of compatibility options turned on. A v2 caller marshaling a type with a v1 MarshalJSON method works, and a v1 caller picks up a dependency's v2 MarshalJSONTo. Your dependencies can migrate on their own schedule.
You can also take v2 defaults with one v1 behavior kept, or the reverse:
// v2 semantics, but tolerate duplicate names for now
json.Marshal(v, json.DefaultOptionsV2(), jsontext.AllowDuplicateNames(true))
// v1 semantics, but reject duplicate names
json.Marshal(v, jsonv1.DefaultOptionsV1(), jsontext.AllowDuplicateNames(false))
The encoding/json docs list each behavior difference and the option that controls it, and a migration guide walks through common cases.
This week
- Upgrade a service to Go 1.27 and switch one decode path to
json.UnmarshalRead. Run your tests and look for duplicate-key, UTF-8 and case-mismatch errors. - Before you change a public API to v2, diff a few responses. Empty slices turning into
[]andomitemptydropping""can break strict clients. - Search for
time.Durationfields in JSON structs and choose a format option before v2 returns errors on them. - Profile your custom
UnmarshalJSONmethods. Any that recurse intojson.Unmarshalare candidates forUnmarshalJSONFrom. - Leave working v1 code alone where nothing pushes you to move. The Go team has set no end date for v1 support.
Volodymyr Chornous

