What Go 1.27's encoding/json/v2 means for your code
Go 1.27 makes encoding/json/v2 stable. It changes defaults for duplicate keys, nil slices and field matching. Here is what to check before you switch.
Go 1.27 ships two new standard library packages, encoding/json/v2 and encoding/json/jsontext. According to the Go Blog post introducing the new JSON API, both are now stable and generally available. The old encoding/json package stays where it is. What decides whether this matters to you is a short list of changed defaults. Version 2 rejects duplicate object keys, writes nil slices and maps as [] and {}, and matches JSON keys to struct fields case-sensitively. If you change an import and nothing else, JSON that used to work can behave differently.
The background you need
Marshaling and unmarshaling. Marshaling turns a Go value, such as a struct, into JSON bytes. Unmarshaling goes the other way. The Go Blog draws a line that the old package blurred. Encoding and decoding deal only with JSON syntax: braces, commas, strings, and whether the text is valid. Marshaling and unmarshaling deal with meaning: which JSON key fills which struct field.
Struct tags. These are the backtick annotations on struct fields, such as json:"name,omitempty". They tell the package what key to use and when to leave a field out. omitempty is the tag option that drops a field from the output when it is considered empty.
The Go 1 compatibility promise. The Go project guarantees that code written against Go 1 keeps compiling and behaving the same in later releases. That is why your old code still builds. It also means the Go team cannot quietly change how encoding/json behaves, even where it behaves badly. The blog says the problems are inherent in the existing API, which makes them practically impossible to fix within that promise.
Why a /v2 path. In Go, an incompatible new version of a package gets a new import path ending in /v2. Old and new can then live side by side. Think of a government form that gets a revised edition. The old edition stays valid and the new one has new rules. The analogy breaks in one way: you can use both editions in the same program at once, even in the same file under different import names.
Nil versus empty. A Go slice or map can be nil, meaning it was never allocated, or empty, meaning allocated but holding nothing. Both have length zero. Version 1 writes a nil slice as JSON null, not []. Many JSON libraries in other languages refuse to load null into a field they expect to be a list. On the other end, that shows up as a crash or a validation error.
Duplicate keys. A JSON object can list the same key twice, for example a role key set first to user and then to admin. The JSON standard, RFC 8259, does not say which value wins. Two parsers can disagree, and an attacker can exploit that gap. The Go Blog cites CVE-2017-12635 as a real case.
Streaming. A streaming parser handles JSON piece by piece instead of holding the whole document in memory. The old MarshalJSON and UnmarshalJSON methods force whole-value handling. According to the blog, when these methods call Marshal or Unmarshal recursively, performance becomes quadratic. In plain terms, the cost grows much faster than the input size.
Last year the Go team released this API as an experiment. Go 1.27 is the release where it becomes stable.
What actually changed in Go 1.27
New:
jsontextis a syntax-only layer. It has a truly streamingEncoderandDecoderand does not use reflection.encoding/json/v2addsMarshalWriteandUnmarshalRead, which work directly on anio.Writerorio.Reader. Options are now an argument to every marshal and unmarshal function. In v1 you could set options only on the Encoder and Decoder types.- The new interfaces
MarshalerToandUnmarshalerFromlet your types read and write through the streaming encoder and decoder. Options pass down the call stack with them. WithMarshalersandWithUnmarshalerslet the caller override how any type is serialized, even types you do not own.
Changed defaults in v2, according to the Go Blog:
- Duplicate names in a JSON object cause an error.
- Nil slices and maps marshal as
[]and{}. - Matching JSON names to struct fields is case-sensitive.
- omitempty now omits a field only if it would encode as
null, an empty string,[], or{}. - Marshaling a
time.Durationreports an error unless you pick a format through an option.
On nil slices, the blog says: “A survey indicated that most Go users prefer that nil slices and maps are marshaled as an empty JSON array or object by default.”
Already true, and still true: encoding/json is unchanged in what you can rely on. The Marshaler and Unmarshaler interfaces are identical in v2. v1’s RawMessage is now a type alias for jsontext.Value, so the two are interchangeable.
Performance: According to the Go Blog, Marshal is roughly at parity with v1, sometimes slightly faster and sometimes slightly slower. Unmarshal is significantly faster, with benchmarks showing improvements of up to 10x. The blog says Kubernetes saw orders-of-magnitude gains in one service after it switched recursive OpenAPI parsing to UnmarshalJSONFrom. The team says to expect more optimization in Go 1.28.
What it means for you
If you do nothing, nothing breaks. v2 is opt-in. The changed defaults matter at the moment you migrate. Each one maps to a concrete failure you can test for.
- Clients sending odd capitalization. If a client sends
userIdbut your field resolves toUserID, v1 matched it anyway. The blog calls this behavior “a surprising default, a potential security vulnerability, and a performance limitation.” Under v2 that field silently stays at its zero value. Check your request logs before you assume every client uses exact casing. - Consumers that check for null. Front-end code or downstream services may test for
nullwhere they now get[]. Usually that is an improvement, but golden-file tests and snapshot tests will flag it. - omitempty fields. A
boolset to false or anintset to 0 encodes as false or 0. Neither is on the v2 list of empty values, so v2 keeps those fields where v1 dropped them. Payload shapes can change. - Duration fields. Any struct with a
time.Durationneeds an explicit option, or marshaling fails. - Duplicate keys. Input that v1 accepted now errors. This is the safer default, but it can surface sloppy upstream producers.
The blog says most behavior changes have a struct tag option or a caller-specified option to keep v1 semantics. You can migrate incrementally: switch the import, restore old behavior where a test fails, then remove those overrides one at a time.
If you have custom UnmarshalJSON methods on hot paths, adding UnmarshalJSONFrom is where the blog points for the largest gains.
What to watch next: the Migrating to v2 guide the blog points to, and the further performance work the Go team says is coming in Go 1.28.
What we don’t know yet
- The future of v1. The post does not say whether
encoding/jsonwill be deprecated or how long it will be maintained as-is. - Library adoption. Web frameworks and SDKs that marshal on your behalf choose their own timeline. The post does not say how quickly they will move or how mixed v1 and v2 code paths will interact in practice.
- Invalid UTF-8. The blog says the default should report an error for invalid UTF-8. It does not include this in its most-notable list of v2 behavior changes. Check the package documentation for the exact default.
- Your workload’s speedup. Up to 10x is a ceiling from benchmarks, not a promise for your payloads.
- Other options. The blog says several v2 options are not covered in the post.
FAQ
Do I have to change anything when I upgrade to Go 1.27?
Can I use v1 and v2 in the same program?
Will v2 make my service faster?
Related tools
Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.
Related reading
2026-11-08
What the Copilot code review settings API means for you
GitHub opened Copilot code review settings to its APIs, but org and enterprise endpoints reject GitHub Apps and fine-grained tokens.
2026-10-30
What GitHub Copilot Fusion means for your model choice
Copilot Fusion picks and combines models for you. What it is, what it replaced, and why its per-model billing is what matters.
2026-10-25
What GitHub App enterprise installs mean for PATs
GitHub Apps can now be installed on a whole enterprise. Here is why that matters if your admin scripts still run on a classic personal access token.
2026-10-21
What GitHub's package firewall means for your CI
GitHub's new package firewall screens installs before they reach your runners, but only for endpoints you reconfigure. Here is what that means.
2026-10-17
What interactive refactoring in gopls means for Go devs
gopls v0.24.0 refactorings can now ask you questions first. It relies on a proposed LSP change, which decides whether your editor gets it.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.