Working Efficiently with JSON in Go
By: Alexander Yaremchuk
Tags: Benchmark, Golang, Microservices, Json, API

JSON (JavaScript Object Notation) is the most popular format for exchanging data between a server and a client. It is scalable, flexible and supported in most programming languages using powerful libraries.

At TourRadar, we use JSON as a communication format between microservices. With more and more services sending data back and forth, the question of speeding up JSON processing becomes increasingly relevant. Speed is our main factor because it affects customer experience and SEO.

According to the official Go documentation, to decode or encode JSON data we should use the Unmarshal and Marshal functions respectively. So in this manual, the terms marshalling and encoding are used interchangeably.

In this article, we compare the most popular and effective fast encoding and decoding techniques in Go that we use in the company.

Accelerating encoding/json using json-iterator/go

Golang has a standard package, encoding/json, that allows easy encoding and decoding. However, this package relies heavily on reflection, which leads to low performance in high-load systems.

Just like the standard package, json-iterator/go is based on reflection, but it claims to have better performance and speed. It acts as a drop-in replacement of the standard library.

Unlike encoding/json, though, this library is customizable and it has some configuration presets. For example, using jsoniter.ConfigFastest it is even more performant.

type Config struct { IndentionStep int MarshalFloatWith6Digits bool EscapeHTML bool SortMapKeys bool UseNumber bool DisallowUnknownFields bool TagKey string OnlyTaggedField bool ValidateJsonRawMessage bool ObjectFieldMustBeSimpleString bool CaseSensitive bool MaxDepth int }

Fast decoding of defined structures using easyjson

If we have defined structures, we can use the binary encoder — easyjson. Instead of using reflection, it generates structure-specific marshal/unmarshal functions using a built-in tool. This library aims to keep the generated Go code simple enough so that it can be easily optimized or fixed.

Using the following command we can generate encoding/decoding functions:

easyjson -all <filename>.go func easyjsonD12cb6aaEncodePkgModelsDb2(out *jwriter.Writer, in TourTransport) { out.RawByte('{') first := true _ = first { const prefix string = ",\"id\":" out.RawString(prefix[1:]) out.Int(int(in.ID)) } { const prefix string = ",\"name_en\":" out.RawString(prefix) out.String(string(in.NameEn)) } { const prefix string = ",\"seo_name\":" out.RawString(prefix) out.String(string(in.SeoName)) } { const prefix string = ",\"type_id\":" out.RawString(prefix) out.Int(int(in.TypeID)) } out.RawByte('}') }

Decoding JSON without schema, reflection and code generation using fastjson

We can speed up JSON encoding/decoding using a direct string splitting technique. The following approach doesn’t implement marshalling and unmarshalling, it just performs functions for working with string variables in JSON format.

Fastjson parses arbitrary JSON without code generation, schema, and reflection. It quickly extracts part of the original JSON with Value.Get(…) and supports raw JSON manipulation. It can parse arrays containing values of distinct types (aka non-homogenous types) — for example, it easily parses the following JSON array [123, “foo”, [456], {“k”: “v”}, null].

Usage example:

s := []byte(`{"foo": [123, "bar"]}`) fmt.Printf("foo.0=%d

", fastjson.GetInt(s, "foo", "0")) // Output: // foo.0=123

Benchmarks

To test the speed of each library we used our personal laptops — MacBook Pro (13-inch, 2018), 2,3 GHz Intel Core i5, 16 GB RAM.

We took a simple tour data API response — 15 items (1.2 MB) — and decoded it using different libraries to compare the speed. We also compared it to PHP’s json_decode function.

Speed comparison of different libraries and languages

Conclusion

Our past experience of working with all of the aforementioned libraries and benchmarks leads us to conclude that the choice of marshalling/unmarshalling method heavily depends on the type of data we’re going to work with. And we recommend mixing different methods depending on the use-cases. In a nutshell:

encoding/json (standard library) is a good solution for working with small objects.

easyjson is the perfect solution if we have a well-defined structure with a lot of data.

fastjson helps improve the speed when we only need some parts of the JSON structure. However, it can’t decode JSON into objects, it just creates a fieldset.

json-iterator can help replace the standard library when we have big objects.

These conclusions helped us to fix some of our own bottlenecks during JSON encoding/decoding:

fastjson helped us to go from 15 to 1 ms during ElasticSearch terms aggregations decoding.

during ElasticSearch terms aggregations decoding. easyjson saved us 50% of the time for decoding of ElasticSearch results.

of the time for decoding of ElasticSearch results. easyjson helped us to encode a JSON response in 2 ms instead of 30 ms .

. json-iterator saved 30% of the time on encoding/decoding items cached in Redis.

of the time on encoding/decoding items cached in Redis. Every denormalized tour object has all the available departures. We use fastjson to find the relevant ones for the search and parse only needed information. It increased the speed by 50%.

Hopefully, this has helped you gain a better understanding of how to work with JSON efficiently in Go. And we would love to hear about your experience in the comments.

UPDATE (2019.12.23). Another drop-in replacement for encoding/json was recommended by the Reddit community — https://github.com/segmentio/encoding. It encodes in 4.5 ms (-40% compared to json-iterator/go) and decodes in 7.7 ms (-13% compared to json-iterator/go).

UPDATE (2019.12.24).By re-using fastjson.ParserPool we went from 5.1 ms to 1.6 ms thanks to the recommendation from Reddit.