GoSuda

Go 1.25 encoding/json v1 vs v2 Comparison

By lemonmint
views ...

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:

  1. Enhanced Accuracy and Predictability: By applying stricter rules by default (e.g., case-sensitivity, prohibition of duplicate keys), it reduces unexpected behaviors.
  2. Performance Improvement: The parsing and encoding engines have been redesigned to increase efficiency.
  3. 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 the FirstName 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 with json.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 type bool is omitted, but in v2, false is not an empty JSON value and thus is not omitted. v2 adds the omitzero option to replace v1's omitempty behavior for 0 or false.
  • 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 and nil maps are marshalled as null.
  • v2 Behavior: By default, nil slices are marshalled as [] (empty array), and nil 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 via json.FormatNilSliceAsNull(true) or json.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 as int64 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 or json.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 in decode.go and a manually written state machine in scanner.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 upon jsontext.
    • 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 the Marshaler/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), and FormatNilSliceAsNull.

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 to jsonv2 by introducing v2's strict behaviors through DefaultOptionsV1().