Skip to content
HN On Hacker News ↗

A new Go API for JSON - The Go Programming Language

▲ 353 points • 178 comments • by olexsmir • 20h ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is human-written.

0 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 1 of 1
SEGMENTS · AI 0 of 1
WORD COUNT 1,508
PEAK AI % 0% · §1
Analyzed
Nov 4
backend: pangram/v3.3
Segments scanned
1 windows
avg 1508 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,508 words · 1 segments analyzed

Human AI-generated
§1 Human · 0%

Michael Pratt, Joe Tsai, Daniel Martí, Johan Brandhorst-Satzkorn, Roger Peppe, Chris Hines, and Damien Neil 4 November 2026 Introduction JavaScript Object Notation (JSON) is a simple data interchange format, the most popular data format used on the internet, and widely read and written by Go programs. Go has included support for JSON in the standard library since well before Go 1.0. We first we wrote about support for JSON in Go more than 15 years ago! Over time, packages evolve with the needs of their users, and encoding/json is no exception. Last year, we wrote about a new experimental API for JSON designed to bring long-awaited improvements and fixes. After additional feedback and refinement, the new encoding/json/v2 and encoding/json/jsontext packages are stable and generally available in Go 1.27! This post is an updated version of the article introducing the experiment, describing the problems with encoding/json, and an introduction to the new packages. Problems with encoding/json Overall, encoding/json has held up well. The idea of marshaling and unmarshaling arbitrary Go types with some default representation in JSON, combined with the ability to customize the representation, has proven to be highly flexible. However, in the years since its introduction, various users have identified numerous shortcomings. Behavior flaws There are various behavioral flaws in encoding/json: Imprecise handling of JSON syntax: Over the years, JSON has seen increased standardization in order for programs to properly communicate. Generally, decoders have become stricter at rejecting ambiguous inputs, to reduce the chance that two implementations will have different (successful) interpretations of a particular JSON value. encoding/json currently accepts invalid UTF-8, whereas the latest Internet Standard (RFC 8259) requires valid UTF-8. The default behavior should report an error in the presence of invalid UTF-8, instead of introducing silent data corruption, which may cause problems downstream. encoding/json currently accepts objects with duplicate member names. RFC 8259 does not specify how to handle duplicate names, so an implementation is free to choose an arbitrary value, merge the values, discard the values, or report an error. The presence of a duplicate name results in a JSON value without a universally agreed upon meaning. This could be exploited by attackers in security applications and has been exploited before (as in CVE-2017-12635). The default behavior should err on the side of safety and reject duplicate names. Leaking nilness of slices and maps: JSON is often used to communicate with programs using JSON implementations that do not allow null to be unmarshaled into a data type expected to be a JSON array or object. Since encoding/json marshals a nil slice or map as a JSON null, this may lead to errors when unmarshaling by other implementations. A survey indicated that most Go users prefer that nil slices and maps are marshaled as an empty JSON array or object by default. Case-insensitive unmarshaling: When unmarshaling, a JSON object member name is resolved to a Go struct field name using a case-insensitive match. This is a surprising default, a potential security vulnerability, and a performance limitation. Inconsistent calling of methods: Due to an implementation detail, MarshalJSON methods declared on a pointer receiver are inconsistently called by encoding/json. While regarded as a bug, this cannot be fixed as too many applications depend on the current behavior. API deficiencies The API of encoding/json can be tricky or restrictive: It is difficult to correctly unmarshal from an io.Reader. Users often write json.NewDecoder(r).Decode(v), which fails to reject trailing junk at the end of the input. Options can be set on the Encoder and Decoder types, but cannot be used with the Marshal and Unmarshal functions. Similarly, types implementing the Marshaler and Unmarshaler interfaces cannot make use of the options and there is no way to plumb options down the call stack. For example, the Decoder.DisallowUnknownFields option loses its effect when calling a custom UnmarshalJSON method. The Compact, Indent, and HTMLEscape functions write to a bytes.Buffer instead of something more flexible like a []byte or io.Writer. This limits the usability of those functions. Performance limitations Setting aside internal implementation details, the public API commits it to certain performance limitations: MarshalJSON: The MarshalJSON interface method forces the implementation to allocate the returned []byte. Also, the semantics require that encoding/json verify that the result is valid JSON and also to reformat it to match the specified indentation. UnmarshalJSON: The UnmarshalJSON interface method requires that a complete JSON value be provided (without any trailing data). This forces encoding/json to parse the JSON value to be unmarshaled in its entirety to determine where it ends before it can call UnmarshalJSON. Afterwards, the UnmarshalJSON method itself must parse the provided JSON value again. Lack of streaming: Even though the Encoder and Decoder types operate on an io.Writer or io.Reader, they buffer the entire JSON value in memory. The Decoder.Token method for reading individual tokens is allocation-heavy and there is no corresponding API for writing tokens. Furthermore, if the implementation of a MarshalJSON or UnmarshalJSON method recursively calls the Marshal or Unmarshal function, then the performance becomes quadratic. Trying to fix encoding/json directly Introducing a new, incompatible major version of a package is a heavy consideration. If possible, we should try to fix the existing package. While it is relatively easy to add new features, it is difficult to change existing features. Unfortunately, these problems are inherent consequences of the existing API, making them practically impossible to fix within the Go 1 compatibility promise. We could in principle declare separate names, such as MarshalV2 or UnmarshalV2, but that is tantamount to creating a parallel namespace within the same package. This leads us to encoding/json/v2 (henceforth called v2), where we can make these changes within a separate v2 namespace in contrast to encoding/json (henceforth called v1). Building on encoding/json/jsontext Before discussing the v2 API, we first introduce the encoding/json/jsontext package that lays the foundation for improvements to JSON in Go. JSON serialization in Go can be broken down into two primary components: syntactic functionality that is concerned with processing JSON based on its grammar, and semantic functionality that defines the relationship between JSON values and Go values. We use the terms “encode” and “decode” to describe syntactic functionality and the terms “marshal” and “unmarshal” to describe semantic functionality. We aim to provide a clear distinction between functionality that is purely concerned with encoding versus that of marshaling. This diagram provides an overview of this separation. Purple blocks represent types, while blue blocks represent functions or methods. The direction of the arrows approximately represents the flow of data. The bottom half of the diagram, implemented by the jsontext package, contains functionality that is only concerned with syntax, while the upper half, implemented by the json/v2 package, contains functionality that assigns semantic meaning to syntactic data handled by the bottom half. The basic API of jsontext is the following: package jsontext type Encoder struct { ... } func NewEncoder(io.Writer, ...Options) *Encoder func (*Encoder) WriteValue(Value) error func (*Encoder) WriteToken(Token) error type Decoder struct { ... } func NewDecoder(io.Reader, ...Options) *Decoder func (*Decoder) ReadValue() (Value, error) func (*Decoder) ReadToken() (Token, error) type Kind byte type Value []byte func (Value) Kind() Kind type Token struct { ... } func (Token) Kind() Kind The jsontext package provides functionality for interacting with JSON at a syntactic level and derives its name from RFC 8259, section 2 where the grammar for JSON data is literally called JSON-text. Since it only interacts with JSON at a syntactic level, it does not depend on Go reflection. The Encoder and Decoder provide support for encoding and decoding JSON values and tokens. The constructors accept variadic options that affect the particular behavior of encoding and decoding. Unlike the Encoder and Decoder types declared in v1, the new types in jsontext avoid muddling the distinction between syntax and semantics and operate in a truly streaming manner. A JSON value is a complete unit of data and is represented in Go as a named []byte. It is identical to RawMessage in v1, which is now a type alias to jsontext.Value. A JSON value is syntactically composed of one or more JSON tokens. A JSON token is represented in Go as the opaque Token type with constructors and accessor methods. It is analogous to Token in v1 but is designed represent arbitrary JSON tokens without allocation. To resolve the fundamental performance problems with the MarshalJSON and UnmarshalJSON interface methods, we need an efficient way of encoding and decoding JSON as a streaming sequence of tokens and values. In v2, we introduce the MarshalJSONTo and UnmarshalJSONFrom interface methods that operate on an Encoder or Decoder, allowing the methods’ implementations to process JSON in a purely streaming manner. Thus, the json package need not be responsible for validating or formatting a JSON value returned by MarshalJSON, nor would it need to be responsible for determining the boundaries of a JSON value provided to UnmarshalJSON. These responsibilities belong to the Encoder and Decoder. Introducing encoding/json/v2 Building on the jsontext package, we now introduce the encoding/json/v2 package. It is designed to fix the problems in v1, while remaining familiar to users of the v1 package.