---
id: 61f2009278a5f7366f79395da59447cf
author: lemonmint
title: Analysis of Behavioral Changes in Go 1.25 encoding/json/v2 and the v1 Compatibility Layer
description: This article compares case-sensitive matching, changes in the semantics of omitempty and omitzero, serialization of nil slices and maps, and the related options of encoding/json/v2, introduced as a experimental feature in Go 1.25 (goexperiment.jsonv2).
language: en
date: 2025-06-20T05:38:20.100520237Z
path: /blog/posts/go-1-25-encoding-json-v1-vs-v2-z74a36b08
category: release
---

The `encoding/json` package v2 in Go is a new implementation designed to improve upon several shortcomings of the existing v1 (inconsistency, surprising behavior, performance issues). It is an experimental feature that is enabled through the `goexperiment.jsonv2` build tag.

Most importantly, **when v2 is enabled, v1 operates as a compatibility layer that emulates v1's behavior on top of the v2 implementation**. This is achieved through the `DefaultOptionsV1()` function in the `v2_v1.go` file. In other words, v2 provides options that can fully reproduce v1's behavior, while simultaneously introducing stricter and more predictable new default behaviors.

The main goals of v2 are as follows.

1. **Improved correctness and predictability**: By default, stricter rules (e.g., case-sensitive matching, prohibition of duplicate keys) are applied to reduce unexpected behavior.
2. **Performance improvements**: The parsing and encoding engines have been redesigned to increase efficiency.
3. **Expanded flexibility and control**: A detailed options system is introduced so that developers can finely control how JSON is processed.

---

## Major Semantic and Behavioral Differences

Based on the `v2_test.go` file, the behavioral differences between v1 and v2 are summarized by item.

### 1. Field Name Matching (Case Sensitivity)

- **v1 behavior**: When unmarshaling JSON object members into Go struct fields, matching is performed **case-insensitively**. Both `"FirstName"` and `"firstname"` are mapped to the `FirstName` field.
- **v2 behavior**: By default, matching is **case-sensitive**, and only fields that match exactly are mapped.
- **Reason for change**: Case-insensitive matching can cause unexpected behavior and incurs a performance penalty when handling non-matching fields. v2 adopts clearer and more predictable behavior as the default.
- **Related options**: In v2, case-insensitivity can be explicitly enabled per field using the `json:"...,case:ignore"` tag option, or applied globally with the `json.MatchCaseInsensitiveNames(true)` option.

### 2. Change in Meaning of the `omitempty` Tag Option

- **v1 behavior**: Fields are omitted based on the "empty state" of the Go value. Here, "empty state" means `false`, `0`, nil pointers/interfaces, and arrays/slices/maps/strings with a length of 0.
- **v2 behavior**: Fields are omitted based on the "empty state" of the encoded JSON value. That is, they are omitted when encoded as `null`, `""`, `{}`, or `[]`.
- **Reason for change**: The v1 definition is tied to Go's type system. v2 provides more consistent behavior by basing it on the JSON type system. For example, in v1 a `bool` field with the value `false` is omitted, whereas in v2 `false` is a non-empty JSON value and is not omitted. In v2, the `omitzero` option has been added so that it can replace the v1 behavior where `omitempty` applied to `0` or `false`.
- **Related options**: To obtain the same behavior as v1 in v2, use the `json.OmitEmptyWithLegacyDefinition(true)` option.

### 3. Change in Behavior of the `string` Tag Option

- **v1 behavior**: Applies to fields of numeric, boolean, and string types. The value is re-encoded inside a JSON string (e.g., `int(42)` -> `"42"`). It is not applied recursively to values inside composite types (slices, maps, etc.).
- **v2 behavior**: **Applies only to numeric types**, and it is applied recursively. That is, numbers inside slices such as `[]int` are also all encoded as JSON strings.
- **Reason for change**: The primary purpose of the `string` option is to represent numbers as strings to prevent precision loss in 64-bit integers. The v1 behavior was limited and lacked consistency. v2 focuses on this core purpose and extends the behavior recursively to make it more useful.
- **Related options**: The v1 behavior can be emulated with the `json.StringifyWithLegacySemantics(true)` option.

### 4. Marshaling of `nil` Slices and Maps

- **v1 behavior**: `nil` slices and `nil` maps are marshaled as `null`.
- **v2 behavior**: By default, `nil` slices are marshaled as `[]` (an empty array) and `nil` maps are marshaled as `{}` (an empty object).
- **Reason for change**: `nil` is an implementation detail of the Go language, and exposing it in a language-independent JSON format is undesirable. `[]` and `{}`, which represent empty collections, are more universal representations.
- **Related options**: In v2, the `json.FormatNilSliceAsNull(true)` or `json.FormatNilMapAsNull(true)` options can be used to marshal them as `null` as in v1.

### 5. Array Unmarshaling

- **v1 behavior**: When unmarshaling into a Go array (`[N]T`), no error is raised even if the length of the JSON array differs from the length of the Go array. If the JSON array is shorter, the remaining elements are filled with zero values; if it is longer, the excess is discarded.
- **v2 behavior**: The length of the JSON array must **exactly match** the length of the Go array. Otherwise, an error is raised.
- **Reason for change**: In Go, the length of a fixed-size array often carries important meaning. The v1 behavior can lead to silent data loss. v2 enhances correctness through stricter rules.
- **Related options**: The v1 behavior can be emulated with the `json.UnmarshalArrayFromAnyLength(true)` option.

### 6. Handling of `time.Duration`

- **v1 behavior**: `time.Duration` is internally treated as an `int64` and is encoded as a JSON number in nanoseconds.
- **v2 behavior**: It is encoded as a JSON string in a format such as `"1h2m3s"`, using the `time.Duration.String()` method.
- **Reason for change**: Numeric nanoseconds are less readable, and the standard string representation of `time.Duration` is more useful.
- **Related options**: The v1 behavior can be used through the `json:",format:nano"` tag option or the `json.FormatTimeWithLegacySemantics(true)` option.

### 7. Handling of Invalid UTF-8

- **v1 behavior**: During marshaling/unmarshaling, if a string contains invalid UTF-8 bytes, they are **silently replaced** with the Unicode replacement character (`\uFFFD`).
- **v2 behavior**: By default, an **error is returned** when invalid UTF-8 is encountered.
- **Reason for change**: This prevents silent data corruption and aligns with the stricter JSON standard (RFC 7493).
- **Related options**: The v1 behavior can be emulated with the `jsontext.AllowInvalidUTF8(true)` option.

### 8. Handling of Duplicate Object Member Names

- **v1 behavior**: Allows members with the same name to appear multiple times within a JSON object. The last occurring value overwrites the others.
- **v2 behavior**: By default, an **error is returned** if duplicate member names are present.
- **Reason for change**: RFC 8259 does not define the behavior for duplicate names, so implementations may behave differently. This can be a source of security vulnerabilities. v2 explicitly rejects them to improve correctness and security.
- **Related options**: The v1 behavior can be emulated with the `jsontext.AllowDuplicateNames(true)` option.

---

## Implementation and Architectural Differences

- **v1**: Relies heavily on the `decodeState` in `decode.go` and the manually written state machine in `scanner.go`. This is a monolithic structure in which parsing logic and semantic analysis are tightly coupled.
- **v2**: The architecture is more modular.
  - **`encoding/json/jsontext`**: Provides a low-level, high-performance JSON tokenizer (`Decoder`) and encoder (`Encoder`). This package focuses solely on the syntactic aspects of JSON.
  - **`encoding/json/v2`**: Builds on `jsontext` to handle the semantic conversion between Go types and JSON values.
  - Through this separation, syntactic analysis and semantic analysis are decoupled, improving both code clarity and performance.

---

## New APIs and Features in v2

v2 provides highly flexible control capabilities through the `json.Options` system.

- **`json.Options`**: A set of options that change marshaling/unmarshaling behavior.
- **`json.JoinOptions(...)`**: Merges multiple options into one.
- **`WithMarshalers` / `WithUnmarshalers`**: A powerful feature that allows injecting serialization/deserialization logic for specific types without implementing the `Marshaler`/`Unmarshaler` interfaces. This is particularly useful when handling types from external packages.
- **New options**: Various behavioral controls that were not possible in v1 are now available, such as `RejectUnknownMembers`, `Deterministic(false)`, and `FormatNilSliceAsNull`.

---

## Conclusion

`encoding/json` v2 is a modern implementation that substantially improves **correctness, performance, and flexibility** based on the experience gained from v1. Although the default behavior has become stricter, the sophisticated `Options` system fully supports all of v1's behaviors, so existing code compatibility is maintained while the advantages of v2 can be adopted incrementally.

- For **new projects**, it is recommended to use v2 by default.
- **Existing projects** may continue to use `jsonv1` as is, or adopt a strategy of migrating to `jsonv2` and gradually introducing v2's stricter behavior through `DefaultOptionsV1()`.
