Go 1.25 encoding/json v1 vs v2 Comparison
Go's encoding/json
package v2 represents a new implementation designed to address several shortcomings of the existing v1, including lack of consistency, surprising behaviors, and performance issues. This is an experimental feature activated via the goexperiment.jsonv2
build tag.
Most importantly, when v2 is active, v1 operates as a compatibility layer emulating v1's behavior on top of the v2 implementation. This is achieved through the DefaultOptionsV1()
function in the v2_v1.go
file. In essence, v2 provides options to perfectly replicate v1's behavior, while simultaneously introducing new, stricter, and more predictable default behaviors.
The primary objectives of v2 are as follows:
- Enhanced Accuracy and Predictability: By applying stricter rules by default (e.g., case-sensitivity, prohibition of duplicate keys), it reduces unexpected behaviors.
- Performance Improvement: The parsing and encoding engines have been redesigned to increase efficiency.
- Expanded Flexibility and Control: An elaborate option system has been introduced, allowing developers precise control over JSON processing methods.
Key Semantic/Behavioral Differences
The behavioral differences between v1 and v2 have been organized by item, primarily focusing on the v2_test.go
file.
1. Field Name Matching (Case Sensitivity)
- v1 Behavior: When unmarshalling JSON object members into Go struct fields, it matches them case-insensitively. Both
"FirstName"
and"firstname"
map to theFirstName
field. - v2 Behavior: By default, it matches fields case-sensitively, mapping only exact matches.
- Reason for Change: Case-insensitive matching can lead to unexpected behaviors and cause performance degradation when processing non-matching fields. v2 adopts clearer and more predictable behavior as its default.
- Related Options: In v2, case-insensitivity can be explicitly enabled per field using the
json:"...,case:ignore"
tag option, or globally applied withjson.MatchCaseInsensitiveNames(true)
.
2. Change in Meaning of omitempty
Tag Option
- v1 Behavior: Omits fields 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: Omits fields based on the "empty state" of the encoded JSON value. That is, fields are omitted if they would be encoded as
null
,""
,{}
, or[]
. - Reason for Change: v1's definition is dependent on Go's type system. v2 provides more consistent behavior by basing it on the JSON type system. For example, in v1, a
false
value of typebool
is omitted, but in v2,false
is not an empty JSON value and thus is not omitted. v2 adds theomitzero
option to replace v1'somitempty
behavior for0
orfalse
. - Related Options: To achieve the same behavior as v1 in v2, use the
json.OmitEmptyWithLegacyDefinition(true)
option.
3. Change in Behavior of string
Tag Option
- v1 Behavior: Applies to numeric, boolean, and string type fields. It re-encodes the value inside a JSON string (e.g.,
int(42)
->"42"
). It does not recursively apply to values within composite types (slices, maps, etc.). - v2 Behavior: Applies only to numeric types and is applied recursively. That is, all numbers within slices like
[]int
are also encoded as JSON strings. - Reason for Change: The primary use of the
string
option is to represent numbers as strings to prevent precision loss for 64-bit integers. v1's behavior was limited and inconsistent. v2 focuses on this core use case and extends the behavior recursively to make it more useful. - Related Options: The
json.StringifyWithLegacySemantics(true)
option can mimic v1's behavior.
4. Marshalling of nil
Slices and Maps
- v1 Behavior:
nil
slices andnil
maps are marshalled asnull
. - v2 Behavior: By default,
nil
slices are marshalled as[]
(empty array), andnil
maps as{}
(empty object). - Reason for Change:
nil
is an implementation detail of the Go language, and exposing it in a language-agnostic JSON format is undesirable.[]
or{}
are more universal representations for empty collections. - Related Options: In v2, marshalling as
null
like v1 can be achieved viajson.FormatNilSliceAsNull(true)
orjson.FormatNilMapAsNull(true)
options.
5. Array Unmarshalling
- v1 Behavior: When unmarshalling into a Go array (
[N]T
), no error is generated even if the length of the JSON array differs from the Go array's length. If the length is shorter, remaining space is zero-filled; if longer, excess elements are discarded. - v2 Behavior: The length of the JSON array must exactly match the length of the Go array. Otherwise, an error is generated.
- Reason for Change: In Go, fixed-size arrays often have significant length implications. v1's behavior could lead to silent data loss. v2 increases accuracy with stricter rules.
- Related Options: The
json.UnmarshalArrayFromAnyLength(true)
option can mimic v1's behavior.
6. time.Duration
Processing
- v1 Behavior:
time.Duration
is internally treated asint64
and encoded as a JSON number in nanoseconds. - v2 Behavior: Uses the
time.Duration.String()
method to encode as a JSON string in the format"1h2m3s"
. - Reason for Change: Numeric nanoseconds are less readable, and the standard string representation of
time.Duration
is more useful. - Related Options: The
json:",format:nano"
tag option orjson.FormatTimeWithLegacySemantics(true)
option can be used to achieve v1's behavior.
7. Invalid UTF-8 Handling
- v1 Behavior: When marshalling/unmarshalling, if invalid UTF-8 bytes are present within a string, they are silently replaced with the Unicode replacement character (
\uFFFD
). - v2 Behavior: By default, an error is returned upon encountering invalid UTF-8.
- Reason for Change: To prevent silent data corruption and adhere to stricter JSON standards (RFC 7493).
- Related Options: The
jsontext.AllowInvalidUTF8(true)
option can mimic v1's behavior.
8. Handling of Duplicate Object Member Names
- v1 Behavior: Allows duplicate member names within a JSON object. The last occurring value overwrites previous ones.
- v2 Behavior: By default, an error is returned if duplicate member names are present.
- Reason for Change: RFC 8259 does not define the behavior of duplicate names, leading to varied implementations. This can be a source of security vulnerabilities. v2 explicitly rejects them to enhance accuracy and security.
- Related Options: The
jsontext.AllowDuplicateNames(true)
option can mimic v1's behavior.
Implementation and Architectural Differences
- v1: Heavily relies on
decodeState
indecode.go
and a manually written state machine inscanner.go
. This is a monolithic structure where parsing logic and semantic analysis are tightly coupled. - v2: The architecture is more modular.
encoding/json/jsontext
: Provides low-level, high-performance JSON tokenizers (Decoder
) and encoders (Encoder
). This package focuses solely on the syntactic aspects of JSON.encoding/json/v2
: Handles the semantic conversion between Go types and JSON values, built uponjsontext
.- This separation of syntactic and semantic analysis improves code clarity and performance.
New API and Features of v2
v2 offers highly flexible control capabilities through the json.Options
system.
json.Options
: A collection of options that modify marshalling/unmarshalling behavior.json.JoinOptions(...)
: Merges multiple options into a single set.WithMarshalers
/WithUnmarshalers
: A powerful feature that allows injecting serialization/deserialization logic for specific types without implementing theMarshaler
/Unmarshaler
interfaces. This is particularly useful when processing types from external packages.- New Options: Various behavioral controls not possible in v1 are now available, such as
RejectUnknownMembers
,Deterministic(false)
, andFormatNilSliceAsNull
.
Conclusion
encoding/json
v2 is a modern implementation that significantly improves accuracy, performance, and flexibility based on the experience with v1. While its default behavior is stricter, the sophisticated Options
system fully supports all v1 behaviors, allowing for gradual adoption of v2's advantages while maintaining compatibility with existing code.
- For new projects, it is advisable to use v2 by default.
- Existing projects can continue to use
jsonv1
or adopt a strategy of gradual migration tojsonv2
by introducing v2's strict behaviors throughDefaultOptionsV1()
.