« Back to Index

json.Decoder vs json.Unmarshal

View original Gist on GitHub

Tags: #go #json #serialization

Golang json.Decoder vs json.Unmarshal.md

General Rule of Thumb

json.Unmarshal reads the entire JSON into memory and decodes it directly into a Go variable, where as json.NewDecoder reads JSON data incrementally, parsing one token at a time, which is more memory-efficient with large JSON payloads.

In summary, use json.Unmarshal for simplicity with smaller data, and json.NewDecoder for efficiency with large or streaming data.

Terminology

[!NOTE] This is also sometimes referred to as ‘serialize’ and ‘deserialize`.

Differences in json.Decoder

  1. json.Decoder is for JSON streams.
  2. json.Decoder silently ignores invalid syntax.
  3. json.Decoder does not drain HTTP connections properly.

[!NOTE] The issues with json.Decoder are summarized from https://ahmet.im/blog/golang-json-decoder-pitfalls/

JSON Streams

json.Decoder is for JSON streams (which are just concatenated/or new-line separated JSON values).

Example of a JSON stream:

{"Name": "Ed"}{"Name": "Sam"}{"Name": "Bob"}

Note: the entire content of that stream is not valid JSON (it should be inside a [ ] to be a valid JSON value), BUT it is a valid JSON stream!

Ignores Invalid Syntax

Lots of people have reported unexpected things happening because of how Decoder just silently ignores malformed JSON syntax. But I’ve not had an issue because I don’t really use Decoder so I can’t give a good example of how things can go wrong.

A poor example (which isn’t the same thing actually as silently ignoring malformed JSON syntax) would be that you expect each object in the stream to have an int field, but if it’s missing then to omit the field from the data structure. With Decoder it will utilize the zero value instead of just dropping the field altogether like you can with Unmarshal when using struct tags…

// Field is ignored by this package.
Field int `json:"-"`

// Field appears in JSON as key "myName".
Field int `json:"myName"`

// Field appears in JSON as key "myName" and
// the field is omitted from the object if its value is empty,
// as defined above.
Field int `json:"myName,omitempty"`

// Field appears in JSON as key "Field" (the default), but
// the field is skipped if empty.
// Note the leading comma.
Field int `json:",omitempty"`

Fails to drain HTTP connections

This can slows down HTTP requests up to ~4x (although this is fixed by the time of Go 1.7).

If the HTTP endpoint is responding with a single JSON object and you are calling json.Decoder#Decode() only once (in which case you should be using json.Unmarshal() instead!), it means you are not getting io.EOF returned yet. Therefore you are not terminating json.Decoder by seeing that io.EOF and the response body remains open and therefore the TCP connection (or another Transport used) cannot be returned to the connection pool even though you are done with it.