How to Migrate from `encoding/json` to `encoding/json/v2`: A Complete Guide for Go 1.27+
The recommended migration strategy from encoding/json to encoding/json/v2 involves running both packages side-by-side using import aliases, preserving legacy behavior with jsonv1.DefaultOptionsV1(), and gradually removing compatibility options after verifying downstream consumers accept the stricter defaults.
Go 1.27 introduces encoding/json/v2, a stricter and more interoperable iteration of the standard library's JSON handling. According to the JetBrains/go-modern-guidelines repository, this new package changes wire-format behavior for invalid UTF-8, duplicate keys, and nil slices, making a blind migration risky for existing codebases.
Understanding the Breaking Changes in encoding/json/v2
Before executing your migration strategy from encoding/json to encoding/json/v2, you must understand the behavioral differences that can break existing contracts:
Stricter UTF-8 and Duplicate Key Handling
Unlike the legacy package, encoding/json/v2 rejects invalid UTF-8 in strings and reports errors for duplicate object names within JSON objects. The original encoding/json accepted these inputs silently, potentially masking data integrity issues.
Nil Slice and Map Encoding
The v2 package encodes nil slices and maps as empty JSON arrays/objects ([] and {}) instead of null. This changes the serialized output for structs containing uninitialized collections, which can break APIs expecting null values.
Unsupported Type Errors
encoding/json/v2 reports explicit errors for unsupported Go types used with JSON operations. The legacy package often fell back to generic behavior or panicked in edge cases, while v2 enforces type safety at the boundary.
The Recommended Migration Strategy from encoding/json to encoding/json/v2
As documented in internal/guidelines/guidelines.json (lines 46-52), the JetBrains guidelines explicitly recommend against replacing existing imports blindly. Instead, follow this four-step migration strategy:
-
Leave existing code unchanged to maintain current behavior for consumers that depend on legacy semantics.
-
Introduce both packages using import aliases—alias the legacy import as
jsonv1and usejsonfor the new version to enable side-by-side usage. -
Preserve legacy behavior where needed by passing
jsonv1.DefaultOptionsV1()tojson.Marshalorjson.Unmarshal. This compatibility shim ensures output matches the originalencoding/jsonsemantics while you adopt the v2 API structure. -
Gradually remove compatibility options after confirming that downstream consumers accept the stricter defaults, typically verified through round-trip JSON testing.
Practical Code Examples
Writing New Code with v2 Defaults
For new modules that do not need to maintain backward compatibility, import encoding/json/v2 directly and leverage its stricter defaults:
import "encoding/json/v2"
type Pet struct {
Name string
Nicknames []string
}
// Uses v2's stricter defaults: nil slice renders as []
body, err := json.Marshal(Pet{Name: "Remi"})
// Output: {"Name":"Remi","Nicknames":[]}
Maintaining Backward Compatibility During Migration
When migrating existing services, use the dual-import pattern with DefaultOptionsV1() to prevent breaking changes:
import (
jsonv1 "encoding/json" // legacy alias
json "encoding/json/v2" // new version
)
type Pet struct {
Name string
Nicknames []string
}
// Preserves legacy behavior: nil slice renders as null
body, err := json.Marshal(
Pet{Name: "Remi"},
jsonv1.DefaultOptionsV1(), // compatibility shim
)
// Output: {"Name":"Remi","Nicknames":null}
Transitioning to Strict v2 Behavior
After verifying that consumers accept the new wire format, selectively disable legacy compatibility options:
import (
jsonv1 "encoding/json"
json "encoding/json/v2"
)
type Pet struct {
Name string
Nicknames []string
}
// Removes legacy nil-slice handling while keeping other v1 defaults
body, err := json.Marshal(
Pet{Name: "Remi"},
jsonv1.DefaultOptionsV1(),
json.FormatNilSliceAsNull(false),
)
// Output: {"Name":"Remi","Nicknames":[]}
Key Source Files and Guidelines
The authoritative guidance for this migration strategy resides in specific files within the JetBrains/go-modern-guidelines repository:
-
internal/guidelines/guidelines.json(lines 46-52): Contains the canonical"json_v2"guideline entry specifying the staged migration approach. -
FEATURES.md(lines 136-152): Summarizes the migration recommendation in prose format, explaining the rationale behind preserving legacy behavior during transitions. -
CHANGELOG.md(line 18): Records the addition of this guidance for Go 1.27+, providing version context for when these recommendations became relevant.
Summary
-
Do not perform a global search-and-replace of
encoding/jsonimports, asencoding/json/v2changes wire-format behavior for nil values, UTF-8 validation, and duplicate keys. -
Do use import aliases (
jsonv1for legacy,jsonfor v2) to run both packages simultaneously during the migration period. -
Do pass
jsonv1.DefaultOptionsV1()to v2 marshal/unmarshal calls when you need to preserve exact legacy output for existing consumers. -
Do verify downstream compatibility before removing
DefaultOptionsV1(), as the stricter defaults in v2 may require updates to API contracts or client implementations.
Frequently Asked Questions
Can I safely drop-in replace encoding/json with encoding/json/v2?
No, you cannot safely perform a drop-in replacement. According to the guidelines in internal/guidelines/guidelines.json, the v2 package introduces stricter defaults that change serialized output—particularly for nil slices and maps, invalid UTF-8, and duplicate keys. Blind replacement risks breaking API contracts with downstream consumers.
How do I maintain backward compatibility while using the v2 API?
Pass jsonv1.DefaultOptionsV1() as an argument to json.Marshal or json.Unmarshal calls. This compatibility shim, defined in the legacy package but consumed by v2, instructs the new marshaler to emulate the exact behavior of encoding/json while you transition to the new API structure.
What version of Go supports encoding/json/v2?
The encoding/json/v2 package and its associated migration guidelines target Go 1.27+, as recorded in CHANGELOG.md line 18. Earlier versions of Go do not include this package in the standard library.
When should I remove DefaultOptionsV1() from my code?
Remove DefaultOptionsV1() only after you have verified through testing that all downstream consumers accept the stricter v2 defaults—specifically empty JSON arrays for nil slices, errors for invalid UTF-8, and rejection of duplicate object keys. This verification typically involves round-trip JSON testing and API contract validation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →