encoding/json/v2 vs encoding/json in Go 1.27+: Impact and Migration Guide

TLDR: The encoding/json/v2 module introduced in Go 1.27+ enforces strict validation that rejects invalid UTF-8 strings, duplicate object keys, and type mismatches, while changing nil slice encoding from null to empty arrays [], creating high-impact wire-format incompatibilities with the legacy encoding/json package.

The JetBrains/go-modern-guidelines repository classifies the adoption of encoding/json/v2 as a High impact change for Go 1.27+ projects, noting that an import change can alter wire behavior and break existing API consumers. Understanding the precise behavioral differences between encoding/json/v2 vs encoding/json is critical for maintaining system compatibility during migration.

Key Behavioral Differences

According to internal/guidelines/guidelines.json (lines 46-53), the encoding/json/v2 package introduces several breaking changes compared to v1:

  • Invalid UTF-8 rejection: While encoding/json silently allows and replaces invalid UTF-8 sequences, encoding/json/v2 rejects invalid UTF-8 and returns an explicit error.
  • Duplicate key detection: encoding/json keeps the last occurrence of duplicate object fields and drops earlier ones silently; encoding/json/v2 fails with an error when encountering duplicate keys.
  • Nil slice encoding: encoding/json encodes nil slices and maps as null; encoding/json/v2 encodes them as empty arrays [] or objects {} by default.
  • Type mismatch reporting: encoding/json may succeed silently or produce unexpected output when unmarshaling into invalid Go types; encoding/json/v2 detects and reports mismatched types immediately.
  • Wire format compatibility: Changing imports without adjustment alters the serialized output, potentially breaking clients expecting the legacy format.

Migration Strategy

The repository recommends distinct approaches based on project maturity, as documented in the guideline source files.

New Projects

For new Go 1.27+ codebases, import encoding/json/v2 directly to benefit from stricter defaults and improved error detection:

import "encoding/json/v2"

type Pet struct {
    Name      string
    Nicknames []string
}

// Encodes nil Nicknames as [] instead of null
body, err := json.Marshal(Pet{Name: "Remi"})
// Output: {"Name":"Remi","Nicknames":[]}

Existing Codebases

For legacy systems, the guidelines in internal/guidelines/guidelines.json (lines 94-107) recommend preserving compatibility using jsonv1.DefaultOptionsV1():

import (
    jsonv1 "encoding/json"
    json   "encoding/json/v2"
)

type Pet struct {
    Name      string
    Nicknames []string
}

// Preserves legacy wire format (null for nil slices)
body, err := json.Marshal(
    Pet{Name: "Remi"},
    jsonv1.DefaultOptionsV1(),
)
// Output: {"Name":"Remi","Nicknames":null}

Progressive Adoption Path

After validating consumer compatibility, you can progressively adopt v2 behaviors. As shown in lines 124-146 of the guidelines JSON, combine compatibility options with specific v2 overrides:

import (
    jsonv1 "encoding/json"
    json   "encoding/json/v2"
)

type Pet struct {
    Name      string
    Nicknames []string
}

// Maintain v1 compatibility except for nil slice encoding
body, err := json.Marshal(
    Pet{Name: "Remi"},
    jsonv1.DefaultOptionsV1(),
    json.FormatNilSliceAsNull(false), // Switch to v2 behavior: [] instead of null
)

Source Code Reference

The implementation details and migration patterns are defined across the following files in the JetBrains/go-modern-guidelines repository:

Summary

  • encoding/json/v2 rejects invalid UTF-8, duplicate keys, and type mismatches that v1 silently accepts.
  • Nil slices encode as [] in v2 versus null in v1, altering wire format compatibility.
  • The JetBrains/go-modern-guidelines repository rates this migration as High impact due to potential client-breaking changes.
  • Use jsonv1.DefaultOptionsV1() (imported as an alias) to preserve legacy behavior while adopting the v2 package.
  • Reference internal/guidelines/guidelines.json for authoritative configuration details and line-specific examples.

Frequently Asked Questions

What is the difference between encoding/json and encoding/json/v2 in Go?

encoding/json/v2 is a new module introduced in Go 1.27+ that provides stricter JSON validation and altered serialization defaults compared to the v1 package. While v1 allows invalid UTF-8 and silently handles duplicate keys, v2 rejects these conditions with errors and changes nil slice encoding from null to empty arrays.

Why does encoding/json/v2 change nil slice encoding?

The v2 module encodes nil slices as empty arrays [] (and nil maps as {}) by default to provide more consistent round-trip behavior and avoid the ambiguity of null values in JSON APIs. This change improves type safety but breaks compatibility with systems expecting the legacy null representation.

How can I migrate to encoding/json/v2 without breaking existing clients?

Import both packages using aliases, then pass jsonv1.DefaultOptionsV1() to your v2 Marshal calls to preserve the original wire format. After testing consumer compatibility, gradually remove compatibility options or apply specific overrides like json.FormatNilSliceAsNull(false) to adopt modern defaults.

Where are the official guidelines for encoding/json/v2 impact documented?

The authoritative source is internal/guidelines/guidelines.json in the JetBrains/go-modern-guidelines repository, specifically lines 46-53 for impact assessment and lines 94-146 for migration examples. This file is loaded by internal/guidelines/guidelines.go and validated by internal/guidelines/guidelines_test.go.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →