# How to Migrate from `encoding/json` to `encoding/json/v2`: A Complete Guide for Go 1.27+

> Learn the recommended migration strategy from encoding/json to encoding/json/v2 in Go 1.27+. Run both side-by-side, preserve legacy behavior, and gradually adopt stricter defaults.

- Repository: [JetBrains/go-modern-guidelines](https://github.com/jetbrains/go-modern-guidelines)
- Tags: migration-guide
- Published: 2026-09-04

---

**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`](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/guidelines/guidelines.json) (lines 46-52), the JetBrains guidelines explicitly recommend against replacing existing imports blindly. Instead, follow this four-step migration strategy:

1. **Leave existing code unchanged** to maintain current behavior for consumers that depend on legacy semantics.

2. **Introduce both packages using import aliases**—alias the legacy import as `jsonv1` and use `json` for the new version to enable side-by-side usage.

3. **Preserve legacy behavior** where needed by passing `jsonv1.DefaultOptionsV1()` to `json.Marshal` or `json.Unmarshal`. This compatibility shim ensures output matches the original `encoding/json` semantics while you adopt the v2 API structure.

4. **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:

```go
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:

```go
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:

```go
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`](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/guidelines/guidelines.json)** (lines 46-52): Contains the canonical `"json_v2"` guideline entry specifying the staged migration approach.

- **[`FEATURES.md`](https://github.com/JetBrains/go-modern-guidelines/blob/main/FEATURES.md)** (lines 136-152): Summarizes the migration recommendation in prose format, explaining the rationale behind preserving legacy behavior during transitions.

- **[`CHANGELOG.md`](https://github.com/JetBrains/go-modern-guidelines/blob/main/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/json` imports, as `encoding/json/v2` changes wire-format behavior for nil values, UTF-8 validation, and duplicate keys.

- **Do** use import aliases (`jsonv1` for legacy, `json` for 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`](https://github.com/JetBrains/go-modern-guidelines/blob/main/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`](https://github.com/JetBrains/go-modern-guidelines/blob/main/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.