Go JSON v2: A Practical Migration Guide to encoding/json/v2

By Mihir Pipermitwala · 10 min read

⚡ TL;DR

Migrate a Go REST API to encoding/json/v2 in 7 steps: struct tags, omitempty, unknown fields, error handling, and jsontext streaming, plus a v1 vs v2 comparison.

Go JSON v2: A Practical Migration Guide to encoding/json/v2

Go’s JSON story just got its first real rewrite in over a decade. In November 2026 the Go team published the official announcement for encoding/json/v2, the redesign that first shipped as an experiment in Go 1.25 and graduates to a supported API in Go 1.27 (The Go Blog, Nov 2026). I’ve been reading through the proposal and the official migration guide, and the good news is this: for a typical REST API, the migration path is shorter than you’d expect. This post walks through it piece by piece, starting from a handler you’ve probably written a hundred times.

TL;DR

  • json.Marshal(v) and json.Unmarshal(b, v) still compile after you swap the import to encoding/json/v2 — the signature shape didn’t change, so most call sites migrate for free (go.dev, Nov 2026).
  • Unmarshal performance is the headline: the Go team’s benchmarks show v2 decoding up to 10x faster than v1, while marshal stays roughly at parity (The Go Blog, Nov 2026).
  • v2 tightens defaults: object member names match case-sensitively, duplicate names error out, and omitempty now omits any field that encodes as an empty JSON value (null, "", [], {}).
  • A compatibility escape hatch exists — pass jsonv1.DefaultOptionsV1() as an option and v2 behaves exactly like v1, so you can flip behaviors off one at a time.

Go source code being refactored to migrate an HTTP handler from encoding/json to encoding/json/v2

Contents

  1. What Is encoding/json/v2?
  2. What Actually Changes?
  3. How Does a Real Handler Migrate?
  4. What Happens to omitempty and Unknown Fields?
  5. How Do Errors Change?
  6. Does Streaming Get Easier with jsontext?
  7. When Should You Switch?
  8. FAQ

What Is encoding/json/v2?

encoding/json/v2 is a second Go package for JSON that lives next to v1, not instead of it. It ships with a second package, encoding/json/jsontext, that handles pure syntax: reading and writing JSON tokens and values without any Go struct semantics attached (The Go Blog, Nov 2026). That split matters. In v1, marshaling and encoding were one operation; in v2, “marshal” means “turn a Go value into JSON semantics” and “encode” means “write it to a stream,” and you can hook into either layer.

The experiment first landed in Go 1.25 behind the GOEXPERIMENT=jsonv2 flag (The Go Blog, Sep 2025). As of the November 2026 announcement, the API is set to be fully supported in Go 1.27, and both packages fall under the Go 1 compatibility promise. The design goal, per the proposal, was to fix fifteen years of accumulated edge cases without breaking the ecosystem, which is why v1 stays exactly where it is and keeps working.

If you care about the fundamentals behind this kind of API redesign (separating concerns, keeping interfaces small), the same thinking shows up across languages; I’ve written about it in the art of programming patterns in Dart.

What Actually Changes?

Here’s the honest summary: the happy path barely changes, and the edge cases change a lot. The table below covers the differences the official migration guide calls out.

Behaviorencoding/json (v1)encoding/json/v2
Field name matchingCase-insensitive fallbackCase-sensitive by default; MatchCaseInsensitiveNames option to restore v1
Unknown fieldsIgnored unless Decoder.DisallowUnknownFields is setIgnored by default; RejectUnknownMembers option to error
Duplicate object namesSilently allowedError by default; jsontext.AllowDuplicateName option to allow
Invalid UTF-8Silently replaced with U+FFFDReports an error
Nil slice / nil mapEncoded as nullEncoded as [] / {}; json.FormatNilSliceAsNull(false) restores v1
omitemptyOmits zero values, empty strings, and empty slices/mapsOmits fields that encode as empty JSON values: null, "", [], {}
time.DurationEncoded as a number (it’s an int64)Errors — no default representation; you supply one
OptionsOne Encoder/Marshaler structPlain function options passed per call
Custom marshalerMarshalJSON() ([]byte, error)MarshalJSONTo(*jsontext.Encoder) error (streaming); v1 interfaces still honored

Two of these will bite real codebases. Case-sensitive matching means a client sending {"userId": 1} against a struct field tagged json:"userid" now gets an error where v1 silently matched. And the omitempty redefinition changes what disappears from your responses. I’ll cover both below.

How Does a Real Handler Migrate?

Take a boring handler: it decodes a request, saves an order, and writes a response. Here’s the v1 version.

Copy
// BEFORE: encoding/json (v1)
package orders

import (
	"encoding/json"
	"net/http"
	"time"
)

type CreateOrderRequest struct {
	UserID    string    `json:"userId"`
	Items     []string  `json:"items"`
	Coupon    string    `json:"coupon,omitempty"`
	CreatedAt time.Time `json:"-"`
}

func Create(w http.ResponseWriter, r *http.Request) {
	var req CreateOrderRequest
	dec := json.NewDecoder(r.Body)
	dec.DisallowUnknownFields()
	if err := dec.Decode(&req); err != nil {
		http.Error(w, "bad request: "+err.Error(), http.StatusBadRequest)
		return
	}

	resp := map[string]any{"id": "ord_123", "items": req.Items}
	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(resp); err != nil {
		http.Error(w, "encode failed", http.StatusInternalServerError)
	}
}

Step 1: swap the import. Change encoding/json to encoding/json/v2. json.Marshal and json.Unmarshal keep their shapes, so the decode of a []byte body would already work. The Decoder/Encoder types, though, moved to jsontext and gained option arguments.

Step 2: replace the decoder with json.UnmarshalRead or a jsontext.Decoder. Streaming from an io.Reader becomes a one-liner with options attached:

Copy
// AFTER: encoding/json/v2, step by step
import (
	"encoding/json/v2"
	"encoding/json/jsontext"
	"net/http"
	"time"
)

type CreateOrderRequest struct {
	UserID    string    `json:"userId"`
	Items     []string  `json:"items"`
	Coupon    string    `json:"coupon,omitempty"`
	CreatedAt time.Time `json:"-"`
}

func Create(w http.ResponseWriter, r *http.Request) {
	var req CreateOrderRequest
	if err := json.UnmarshalRead(
		r.Body,
		&req,
		json.RejectUnknownMembers(true), // was dec.DisallowUnknownFields()
	); err != nil {
		http.Error(w, "bad request: "+err.Error(), http.StatusBadRequest)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	enc := jsontext.NewEncoder(w)
	if err := enc.WriteValue([]byte(`{"id":"ord_123"}`)); err != nil {
		http.Error(w, "encode failed", http.StatusInternalServerError)
	}
}

Most real handlers would marshal the response struct instead of hand-writing JSON; json.MarshalWrite(w, resp) handles that, and jsontext.WithIndent(" ") is the option that replaces Encoder.SetIndent. That’s the whole migration for this handler: two imports, two call sites, and one option renamed. If your structs follow clean interface boundaries rather than passing JSON blobs around, you’ll feel this even less. It’s the same principle I covered in SOLID principles with Flutter examples.

What Happens to omitempty and Unknown Fields?

These two deserve their own section because they’re where output actually changes.

omitempty, redefined. In v1, omitempty drops a field when it holds a Go zero value: 0, "", false, a nil pointer, or a slice/map with length zero. That’s Go-centric, and it’s surprising: you can’t omit a slice that’s non-nil but empty, and you can’t omit a custom “empty” value. v2 redefines it: a field is omitted when it would encode as an empty JSON value, meaning null, "", [], or {} (The Go Blog, Nov 2026). Practically: nil pointers still drop, empty slices now drop too, and a struct-typed field whose encoding is {} gets dropped as well. The safer mental model is “v2 omits anything that serializes to an empty JSON container.” If you need finer control, the omitzero tag (added to v1 in Go 1.24) gives you explicit, Go-zero-value-based control in both worlds.

Unknown fields. v1’s DisallowUnknownFields had a nasty habit of silently stopping to work the moment a field’s type implemented UnmarshalJSON, because a custom unmarshaler takes over and the decoder’s setting never reaches it (The Go Blog, Nov 2026). v2’s RejectUnknownMembers(true) is a per-call option, and it composes correctly through nested types. Strict APIs, the kind that should fail fast on typos like user_id vs userId, get a real win here.

How Do Errors Change?

v1’s errors were strings with context smeared on. v2 gives you typed errors you can program against. The packages expose distinct error types for syntax problems (malformed JSON found by jsontext) and semantic problems (JSON that’s valid but doesn’t fit your Go types), and the migration guide notes v2 additionally reports errors for structurally invalid situations v1 ignored: duplicate names, invalid UTF-8, a time.Duration with no representation, or a "string" tag on a type where it makes no sense (go.dev, Nov 2026).

That strictness cuts both ways. Your decode paths get better: instead of a 400 with “json: cannot unmarshal,” you can return which byte offset and which member name failed. But your test suite will also start seeing errors on inputs v1 accepted quietly. Budget an afternoon for the error-behavior diff, especially around duplicate names from clients that hand-assemble JSON.

Does Streaming Get Easier with jsontext?

Yes, and this is the part I’d actually migrate for. jsontext is a token-level API: a jsontext.Encoder has WriteToken and WriteValue, and a jsontext.Decoder has ReadToken and ReadValue. You can walk a 10 GB JSON file one token at a time, or peek at just the first field of each object in an NDJSON stream, without allocating structs at all.

For custom types, v2 adds streaming interfaces: implement MarshalJSONTo(*jsontext.Encoder) error or UnmarshalJSONFrom(*jsontext.Decoder) error and your type never materializes an intermediate []byte the way v1’s MarshalJSON forces. The Go team cites the Kubernetes kube-openapi project getting orders-of-magnitude improvements from exactly this switch (The Go Blog, Nov 2026). Types implementing the old v1 interfaces keep working in v2, so this upgrade is optional and per-type.

If you just need to eyeball an API response while debugging, my JSON formatter tool does the same job in the browser.

When Should You Switch?

  1. You’re on Go 1.25 or 1.26: you can try v2 today behind GOEXPERIMENT=jsonv2, but it’s still flagged experimental, so keep it off the critical path. Compile-and-run experiments only.
  2. You’re on Go 1.27 (or will be): v2 is supported, but it’s still a separate import, and nothing forces you off v1. Start with the low-risk move: keep v1, and add v2 types where you want jsontext streaming.
  3. Read-heavy services: the up-to-10x unmarshal improvement is the strongest reason to migrate (The Go Blog, Nov 2026). Independent benchmarks are more mixed: Daniel Lemire’s analysis found some workloads where v2 is roughly 1.5x slower (lemire.me, Aug 2026), so benchmark your own payloads before committing.
  4. APIs with strict request validation: RejectUnknownMembers composing correctly through nested types is a genuine upgrade over DisallowUnknownFields. Worth migrating for on its own.
  5. Everyone else: wait one minor release. The official migration guide’s option-by-option path (DefaultOptionsV1() first, then flip v1 behaviors off one at a time, with the jsonsplit tool logging differences in production) is designed for exactly that pace.

My verdict: v2 is the future of JSON in Go and the migration cost for typical handlers is low, but the case-sensitive matching and omitempty changes mean you should migrate behind a full test suite, not in a drive-by commit.

FAQ

Is encoding/json/v2 available in Go 1.25?

Yes, but only as an experiment. Set GOEXPERIMENT=jsonv2 when building. It becomes a fully supported API in Go 1.27.

Do I have to rewrite all my struct tags?

No. Basic tags like json:"userId" work unchanged in v2. The omitempty semantics shift, and v2 adds new tags such as embed, but most existing tags carry over as-is.

Is json v2 faster than json v1?

Decoding is significantly faster; the Go team reports up to 10x in their benchmarks. Marshaling is roughly at parity, sometimes slightly faster, sometimes slower. Benchmark your own payloads.

Will encoding/json (v1) be removed?

No. v1 remains in the standard library and is now implemented on top of v2 internally. Existing code keeps compiling and behaving the same under the Go 1 compatibility promise.

What’s the safest migration strategy?

Import v2, pass jsonv1.DefaultOptionsV1() to match old behavior, then disable v1 compatibility options one at a time. The official migration guide documents this path, and the jsonsplit tool can log v1-vs-v2 differences in production.

Mihir Pipermitwala

Written by Mihir Pipermitwala

Senior Full Stack Consultant & Mobile Engineering Specialist building user-focused applications in healthcare with Flutter, Dart and React Native. I write about what I actually build and test — every tutorial here comes from real project work.

More about Mihir →
Scroll down to load comments...
Related Blogs
Go JSON v2: A Practical Migration Guide to encoding/json/v2

Go JSON v2: A Practical Migration Guide to encoding/json/v2

Migrate a Go REST API to encoding/json/v2 in 7 steps: struct tags, omitempty, unknown fields, error handling, and jsontext streaming, plus a v1 vs v2 comparison.

GOTUTORIALDEVELOPER TOOLS

November 12, 2026

Prompt Injection Is the New SQL Injection — Part 3: Production Hardening, Monitoring, and the OWASP 2026 Checklist

Prompt Injection Is the New SQL Injection — Part 3: Production Hardening, Monitoring, and the OWASP 2026 Checklist

Part 3 of a 3-part series. Secure AI agents in production — egress filtering against data exfiltration, canary tokens, audit logging, red-teaming in CI/CD, MCP security, and the OWASP 2026 agentic checklist.

AI TOOLSSECURITYLLMAI AGENTSDEVELOPER TOOLS

November 09, 2026

php2shell: Auditing AI-Generated PHP for RCE Vulnerabilities

php2shell: Auditing AI-Generated PHP for RCE Vulnerabilities

php2shell (CVE-2026-109618/109619) made pre-auth RCE a PHP-wide problem. Run this 7-step grep and checklist routine to audit AI-generated PHP before deploy.

SECURITYPHPAI TOOLSDEVELOPER TOOLS

November 08, 2026

Prompt Injection Is the New SQL Injection — Part 2: Defense in Depth, Input Boundaries, and Tool Validation

Prompt Injection Is the New SQL Injection — Part 2: Defense in Depth, Input Boundaries, and Tool Validation

Part 2 of a 3-part series. Build a four-layer defense against prompt injection in Python — input sanitization, contextual isolation, strict tool argument validation with Pydantic, and human-in-the-loop gates.

AI TOOLSSECURITYLLMAI AGENTSDEVELOPER TOOLS

November 07, 2026

Related Tutorials
Essential Git Skills: How to Delete Remote Branches

Essential Git Skills: How to Delete Remote Branches

Keep your repository clean and organized. This guide walks you through how to delete both local and remote branches in Git, step by step.

BITBUCKETDELETE LOCAL BRANCHDELETE REMOTE BRANCHGITGIT BRANCH CLEANUP AUTOMATION

June 12, 2024

Show an Offline Message for No Internet in Flutter

Show an Offline Message for No Internet in Flutter

You have to use the 'Connectivity Flutter Package' to achieve this feature on your App. This package helps to know whether your device is online or offline.

CONNECTIVITY PLUSDEPENDENCIESFLUTTERFLUTTER DEVELOPMENTFLUTTER PACKAGES

April 09, 2024

Mastering TabBar & TabBarView in Flutter: A Complete Guide

Mastering TabBar & TabBarView in Flutter: A Complete Guide

Implement TabBar and TabBarView in Flutter step by step — customize tab indicators, enable scrollable tabs, and change tabs programmatically.

CODINGDEFAULT TAB CONTROLLERFLUTTERLEARN TO CODETAB CONTROLLER

July 26, 2023

File Manipulation in Flutter: Best Practices & Examples

File Manipulation in Flutter: Best Practices & Examples

Master Flutter file manipulation — permission management, directory handling, and practical read-write scenarios to elevate your app's file handling.

CODINGFLUTTERFLUTTER DEVELOPMENTFLUTTER FILE OPERATIONFLUTTER PATH PROVIDER

June 30, 2023

Related Recommended Services
Visual Studio Code for the Web

Visual Studio Code for the Web

Build with Visual Studio Code, anywhere, anytime, in your browser.

IDEVISUAL STUDIOVISUAL STUDIO CODEWEB
Renovate | Automated Dependency Updates

Renovate | Automated Dependency Updates

Renovate Bot keeps source code dependencies up-to-date using automated Pull Requests.

AUTOMATED DEPENDENCY UPDATESBUNDLERCOMPOSERGITHUBGO MODULES
Best XML Formatter and XML Beautifier

Best XML Formatter and XML Beautifier

Online XML Formatter will format xml data, helps to validate, and works as XML Converter. Save and Share XML.

XMLXML BEAUTIFIERXML CONVERTERXML FORMATXML FORMATTER
Kubecost | Kubernetes cost monitoring and management

Kubecost | Kubernetes cost monitoring and management

Kubecost started in early 2019 as an open-source tool to give developers visibility into Kubernetes spend. We maintain a deep commitment to building and supporting dedicated solutions for the open source community.

CLOUDKUBECOSTKUBERNETESOPEN SOURCESELF HOSTED
Related Recommended Stories
How GitHub reduced testing time for iOS apps with new runner features

How GitHub reduced testing time for iOS apps with new runner features

Learn how GitHub used macOS and Apple Silicon runners for GitHub Actions to build, test, and deploy our iOS app faster.

IOSGITHUBTESTINGRUNNER
5 ways to transform your workflow using GitHub Copilot and MCP

5 ways to transform your workflow using GitHub Copilot and MCP

Learn how to streamline your development workflow with five different MCP use cases.

AGENT MODECODING AGENTCOPILOTFIGMAGITHUB
One weird trick for powerful Git aliases

One weird trick for powerful Git aliases

Advanced Git Aliases

ALIASALIAS TEMPLATEATLASSIANBITBUCKETGIT
Awesome Python

Awesome Python

An opinionated list of awesome Python frameworks, libraries, software and resources

AWESOMEAWESOME PYTHONCOLLECTIONSGITHUBPYTHON
Related Recommended Tools
Find out what websites are built with - Wappalyzer

Find out what websites are built with - Wappalyzer

Find out the technology stack of any website. Create lists of websites and contacts by the technologies they use.

ADD ONSANALYTICSAPP STOREAPPLEBOOKING
Sourcetree | Free Git GUI for Mac and Windows

Sourcetree | Free Git GUI for Mac and Windows

A Git GUI that offers a visual representation of your repositories. Sourcetree is a free Git client for Windows and Mac.

GITGITHUBGITLABATLASSIANBITBUCKET
Related Recommended Videos