Key takeaways
- encoding/json/v2 and encoding/json/jsontext are stable in Go 1.27, which arrived in August 2026. The old encoding/json stays supported and you are not required to migrate.
- v2 changes defaults: nil slices and maps become [] and {}, struct field names match case-sensitively, invalid UTF-8 and duplicate names are errors, and omitempty means a different thing.
- Every listed difference has an option that brings the v1 behaviour back, and jsonv1.DefaultOptionsV1() restores all of them at once.
- Go says v2 unmarshaling is significantly faster, with benchmarks showing up to 10x, while marshaling is roughly at parity with v1.
- Since Go 1.27, encoding/json itself runs on the v2 engine with v1 behaviour kept. GOEXPERIMENT=nojsonv2 turns that off for now.
On this page
Go JSON v2 is the encoding/json/v2 package, which became stable in Go 1.27 alongside a new low-level package, encoding/json/jsontext. Compared with the original encoding/json (now called v1), v2 has stricter defaults: it rejects invalid UTF-8 and duplicate object names, marshals nil slices and maps as [] and {}, matches struct field names case-sensitively, and redefines omitempty. v1 is not going away, and you do not have to migrate.
This page puts every documented default change side by side with the option that reverses it, explains the changes most likely to affect a running service, and walks through the three migration paths the Go team recommends.
Go JSON v1 vs v2: every default that changed
The full list of differences lives in the "Migrating to v2" section of the encoding/json docs. The table below covers each one, with the option that brings back the v1 behaviour. Options without a prefix are in encoding/json/v2; jsonv1. means encoding/json; jsontext. means encoding/json/jsontext.
| Behaviour | v1 (encoding/json) | v2 (encoding/json/v2) | Option for v1 behaviour |
|---|---|---|---|
| Nil slice or map | Marshals as null | Marshals as [] or {} | FormatNilSliceAsNull, FormatNilMapAsNull |
| Matching JSON names to struct fields | Case-insensitive | Exact, case-sensitive | MatchCaseInsensitiveNames plus jsonv1.MatchCaseSensitiveDelimiter. Per field, the case:ignore tag option is close to v1 but also ignores dashes and underscores |
omitempty | Omits false, 0, nil pointers and interfaces, and empty arrays, slices, maps and strings | Omits a field that would encode as JSON null, empty string, empty object or empty array | jsonv1.OmitEmptyWithLegacySemantics |
string tag option | Can quote strings, bools and numbers | Only applies to values that encode as JSON numbers | jsonv1.StringifyWithLegacySemantics |
| Invalid UTF-8 | Silently replaced with the Unicode replacement character | Error | jsontext.AllowInvalidUTF8 |
| Duplicate object names | Allowed | Error | jsontext.AllowDuplicateNames |
Go byte arrays ([N]byte) | JSON array of numbers | Base64 string | jsonv1.FormatByteArrayAsArray |
| Unmarshaling into a Go array | Any JSON array length accepted | Length must match exactly | jsonv1.UnmarshalArrayFromAnyLength |
time.Duration | Number of nanoseconds | Runtime error (no default format) | jsonv1.FormatDurationAsNano |
| Map key order when marshaling | Deterministic | Not deterministic | Deterministic |
| HTML and JavaScript characters in strings | Escaped | Minimal escaping | jsontext.EscapeForHTML, jsontext.EscapeForJS |
Pointer-receiver MarshalJSON | Called only on addressable values | Always called | jsonv1.CallMethodsWithLegacySemantics |
| Methods on map keys | Never called | May be called | jsonv1.CallMethodsWithLegacySemantics |
JSON null into a non-empty Go value, and merging | Inconsistent; merges into array and slice elements, struct fields, pointers and interfaces | Always zeroes on null; merges mainly for objects | jsonv1.MergeWithLegacySemantics |
| Invalid struct types (for example a malformed tag) | Never reported at runtime | Runtime error | jsonv1.ReportErrorsWithLegacySemantics |
Each of these options takes a bool, and true selects the v1 behaviour. If you want all of them, jsonv1.DefaultOptionsV1() is documented as "the full set of all options that define v1 semantics."
What JSON v2 is, in plain terms
Go has shipped encoding/json since Go 1. Over the years the Go team found problems in it that could not be fixed without breaking existing programs. The Go blog post announcing v2 (4 November 2026) gives examples: v1 accepts invalid UTF-8, accepts objects with duplicate member names, marshals nil slices and maps as null, and matches JSON names to Go fields case-insensitively. The authors say these problems cannot practically be fixed within the Go 1 compatibility promise, so they built a new package at a new import path.
v2 is split into two layers:
encoding/json/jsontexthandles JSON syntax only: reading and writing tokens and values, with no reflection.encoding/json/v2handles meaning: turning Go values into JSON and back. Its main functions areMarshal,MarshalWrite,MarshalEncode,Unmarshal,UnmarshalReadandUnmarshalDecode, and every one of them accepts options as arguments.
The path to stable took three steps. Go 1.25 shipped v2 as an experiment, enabled with GOEXPERIMENT=jsonv2 at build time, according to the Go 1.25 release notes. The Go 1.27 release notes say Go 1.27 "arrives in August 2026" and list encoding/json/v2 and encoding/json/jsontext as new packages. The blog post followed in November and calls them "stable and generally available in Go 1.27."
The changes most likely to break a service
Nil slices now marshal as []
This is the change most API clients will notice. The migration guide uses a Pet struct with a Nicknames []string field. Under v1, a pet with no nicknames marshals as "Nicknames":null. Under v2 it marshals as "Nicknames":[]. The blog says most Go users the team surveyed preferred the empty array, but a client that checks for null will see a different value.
Field names are case-sensitive
In v1, a JSON member called "NAME" or "name" fills a field named Name. In v2 it must match exactly. The blog calls the old default surprising, a potential security vulnerability and a performance limit. If you accept JSON from clients that are loose about case, either fix the clients or restore the old matching. The encoding/json docs name two options for this difference. MatchCaseInsensitiveNames(true) turns on case-insensitive matching, but on its own v2 then also ignores dashes and underscores. Adding jsonv1.MatchCaseSensitiveDelimiter(true) stops that, so the two together match v1. For a single field, the case:ignore tag option accepts any case too, but it also ignores dashes and underscores, so it is close to v1 rather than the same. Field tag options take precedence over the options you pass to a call.
omitempty means something different, so use omitzero
In v2, omitempty omits a field "if the field value would have been encoded as a JSON null, empty string, empty object, or empty array." A false bool or a 0 number encodes as false or 0, which is none of those, so v2 keeps them. Under v1 they were dropped.
The fix the docs recommend is omitzero, which already works in v1 since Go 1.24. The Go 1.24 release notes say a field with omitzero is omitted when its value is zero, using the type's IsZero() bool method if it has one. They add that, unlike omitempty, omitzero omits a zero time.Time. If both options are set, the field is omitted when it is either empty or zero.
type Order struct {
ID string `json:"id"`
Paid bool `json:"paid,omitzero"`
Shipped time.Time `json:"shipped,omitzero"`
}
The encoding/json docs suggest moving bool, number, pointer and interface fields from omitempty to omitzero, which both versions support, so those fields keep being omitted when zero after the switch. For arrays, slices, maps and strings, the docs say omitempty behaves the same in v1 and v2.
For time.Time, omitzero is new behaviour: v1 omitempty never omitted a zero time, while omitzero does. In the example above, a zero Shipped time is left out, which v1 with omitempty would not have done.
time.Duration fails until you choose a format
The blog says v2 "reports an error when trying to serialize a time.Duration," because it has no default representation. v1 wrote it as a number of nanoseconds. If your structs contain durations, pass jsonv1.FormatDurationAsNano(true) or change how you store them before migrating.
Map order, byte arrays and HTML escaping
Three output changes can break golden-file tests or caches keyed on exact bytes. v1 writes map keys in a deterministic order; v2 does not unless you pass Deterministic(true), which the docs define as "marshaling the same input value will always serialize as the same output bytes." A Go byte array such as [32]byte becomes a Base64 string instead of an array of numbers. And v1 escapes HTML characters in strings, while v2 escapes minimally. jsontext.EscapeForHTML(true) escapes <, > and & so the output is safe to embed in HTML.
Is v2 faster?
For decoding, the Go team says yes. The blog says v2 unmarshaling is "significantly faster than v1," with benchmarks showing improvements of up to 10x, and that "the Marshal performance of v2 is roughly at parity with v1," sometimes slightly faster and sometimes slightly slower. It adds that types moving to the new streaming methods, MarshalJSONTo and UnmarshalJSONFrom, can gain more, and that further improvements are expected in Go 1.28. The benchmarks are in the go-json-experiment/jsonbench repository. These are the Go team's own figures; your gain depends on your data and types.
What changed for v1 users in Go 1.27
Even if you never import v2, your program uses it. The Go 1.27 release notes say encoding/json "is now backed by the v2 implementation. Marshaling and unmarshaling behavior is preserved, but the exact text of error messages may differ." So a test that compares error strings may fail after upgrading.
The two packages also work together. A type that implements v2's MarshalerTo still gets its custom marshaler when called through v1's json.Marshal, and v2 honours v1 MarshalJSON methods. v1's RawMessage is now a type alias for jsontext.Value.
If the new engine causes a problem, the release notes say you can set GOEXPERIMENT=nojsonv2 at build time to restore the original v1 implementation. They also say this opt-out "is expected to be removed in a future release."
How to migrate: three official approaches
The migration guide describes three paths, depending on how much risk you can take.
1. All at once
Change the import to encoding/json/v2, run your tests, fix what fails, and set a compatibility option where you need the old behaviour. This is the guide's Pet example after the change:
package main
import (
"encoding/json/v2"
"fmt"
)
type Pet struct {
Name string
Nicknames []string
}
func main() {
pets := []Pet{
{Name: "Oliver", Nicknames: []string{"Ollie", "Olliepop"}},
{Name: "Remi"},
}
b, err := json.Marshal(pets)
if err != nil {
panic(err)
}
fmt.Println(string(b))
}
The guide gives its output as [{"Name":"Oliver","Nicknames":["Ollie","Olliepop"]},{"Name":"Remi","Nicknames":[]}]. Under v1, Remi's nicknames were null.
2. Option by option
For lower risk, move every call to v2 but pass the full v1 option set, which the guide calls "a trivial and safe change." Then switch v2 behaviours on one at a time. Later options override earlier ones, so this call is v1 behaviour except for nil slices:
import (
jsonv1 "encoding/json"
"encoding/json/v2"
)
b, err := json.Marshal(v, jsonv1.DefaultOptionsV1(), json.FormatNilSliceAsNull(false))
The blog shows the reverse pattern too: start from v2 defaults and keep one v1 behaviour, for example json.Marshal(v, json.DefaultOptionsV2(), jsontext.AllowDuplicateNames(true)).
3. Compare in production with jsonsplit
For production servers, the guide suggests github.com/go-json-experiment/jsonsplit. It can call both versions, return the v1 result, and report any difference to your monitoring, with an option to detect which setting caused it. Once no differences appear, you switch to returning v2 output and finally replace jsonsplit with encoding/json/v2. The guide warns that calling both versions roughly doubles marshal cost, and offers a way to check only a random share of calls.
Which should you choose?
Nothing forces a move. The Go 1.27 release notes say the v1 API will continue to be supported and users are not required to migrate. These cases show where each path fits, using only what the Go docs say.
Stay on encoding/json (v1)
v1 is supported and covered by the Go 1 compatibility promise. In Go 1.27 it is already backed by the v2 implementation with its behaviour preserved, though the exact text of error messages may differ.
New code that wants stricter defaults
Import encoding/json/v2. You get errors on invalid UTF-8 and duplicate names, exact field-name matching, and [] and {} for nil slices and maps.
A service that must not change its output
Move calls to v2 with jsonv1.DefaultOptionsV1(), then switch v2 behaviours on one at a time. The guide calls the first step "a trivial and safe change."
A production server where you want proof
Use jsonsplit to call both versions, return the v1 result and report any difference, then switch to v2 output once none appear.
Heavy unmarshaling
The Go blog says v2 unmarshaling is significantly faster, with benchmarks showing up to 10x. Marshaling is roughly at parity. Your gain depends on your data and types.
Confirmed vs not yet known
- Confirmed:
encoding/json/v2andencoding/json/jsontextare stable in Go 1.27. v1 is supported indefinitely and is now backed by the v2 engine. All the differences in the table above, and their options, are in the Go 1.27 package docs (go1.27.3 on pkg.go.dev). - Announced, no date: the
GOEXPERIMENT=nojsonv2opt-out is expected to be removed "in a future release." Go has not said which one. - Expected: the blog says additional performance improvements are expected in Go 1.28.
For more plain-English explainers on languages and developer tools, see the Software and development section of Dexaloop Tech.
Questions
Do I have to migrate from encoding/json to encoding/json/v2?
No. The Go 1.27 release notes say the v1 API will continue to be supported and users are not required to migrate. The migration guide adds that encoding/json is covered by the Go 1 compatibility promise and will never go away.
Is Go JSON v2 faster than v1?
For unmarshaling, yes, by Go's own figures. The Go blog says v2 unmarshaling is significantly faster, with benchmarks showing improvements of up to 10x, and that marshaling is roughly at parity with v1. The benchmarks are in the go-json-experiment/jsonbench repository.
How do I enable JSON v2 in Go?
On Go 1.27 or later, import encoding/json/v2 (and encoding/json/jsontext if you need the low-level layer). No flag is needed. In Go 1.25 it was an experiment enabled with GOEXPERIMENT=jsonv2 at build time.
What is the difference between omitzero and omitempty?
omitzero, added in Go 1.24, omits a field whose value is zero, using an IsZero() bool method if the type has one. In v2, omitempty omits a field only if it would encode as JSON null, an empty string, an empty object or an empty array. So false and 0 are no longer omitted by omitempty in v2; use omitzero for them.
Does my existing encoding/json code behave differently in Go 1.27?
The release notes say encoding/json is now backed by the v2 implementation, with marshaling and unmarshaling behaviour preserved, but the exact text of error messages may differ. If you hit a compatibility problem, GOEXPERIMENT=nojsonv2 at build time restores the original implementation for now.
Sources
- A new Go API for JSON (The Go Blog, 4 Nov 2026) (accessed 7 November 2026)
- encoding/json/v2 Migration Guide (go.dev) (accessed 7 November 2026)
- Go 1.27 Release Notes (go.dev) (accessed 7 November 2026)
- encoding/json package documentation, Migrating to v2 (pkg.go.dev, go1.27.3) (accessed 7 November 2026)
- encoding/json/v2 package documentation (pkg.go.dev, go1.27.3) (accessed 7 November 2026)
- encoding/json/jsontext package documentation (pkg.go.dev, go1.27.3) (accessed 7 November 2026)
- Go 1.24 Release Notes, encoding/json (go.dev) (accessed 7 November 2026)
- Go 1.25 Release Notes, encoding/json/v2 experiment (go.dev) (accessed 7 November 2026)
Written from the sources above and checked against them before publishing. Found a mistake? Email support@dexaloop.com and we will correct it.