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 post, we provide you with a guide and compare the most popular and effective fast encoding and decoding techniques in Go. We also provide Go code examples to check how the most popular tools deal with encoding/decoding objects of different sizes.
Libraries for accelerating JSON marshalling/unmarshalling
There are several solutions how to use JSON files with Golang:
-
encoding/json (the standard package)
-
ffjson
-
fastjson
-
easyjson
-
json-iterator/go.
Let’s take a quick look at these packages and write a code example for benchmark testing.
encoding/json
Golang has a standard package, encoding/json, that allows for easy and fast encoding and decoding.
Here’s an example of a benchmark for marshalling and unmarshalling JSON objects:
// Benchmark large object marshal method from std package
func BenchmarkStdMarshalLarge(b *testing.B) {
var l int64
for i := 0; i < b.N; i++ {
data, err := json.Marshal(&largeData)
if err != nil {
b.Error(err)
}
l = int64(len(data))
}
b.SetBytes(l)
}
// Benchmark concurrent large object marshal method from std package
func BenchmarkStdMarshalLargeParallel(b *testing.B) {
var l int64
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
data, err := json.Marshal(&largeData)
if err != nil {
b.Error(err)
}
l = int64(len(data))
}
})
b.SetBytes(l)
}
// Benchmark large object unmarshal method from std package
func BenchmarkStdUnmarshalLarge(b *testing.B) {
b.SetBytes(int64(len(largeStructString)))
for i := 0; i < b.N; i++ {
var s LargeStruct
err := json.Unmarshal(largeStructString, &s)
if err != nil {
b.Error(err)
}
}
}
But this package uses reflection while iteratively declaring the member of a structure and defining its type. This leads to low performance with high-load systems.
Binary encoding is the best practice to solve this problem. It’s unavailable in the standard package but is widely used by other Go libraries such as ffjson and easyjson.
ffjson
The main aim of this package is to facilitate JSON serialization with no additional code changes. Ffjson generates static MarshalJSON
and UnmarshalJSON
functions that reduce reliance upon runtime reflection for serialization.
If ffjson doesn’t understand a Type involved, it falls back to encoding/json. This means the package is a safe drop-in replacement.
To generate code, add the following line to your file
ffjson <filename>.go
Here’s an example of statically generated code for marshalling/unmarshalling methods with the ffjson package:
// MarshalJSON marshal bytes to json - template
func (j *LargeStruct) MarshalJSON() ([]byte, error) {
var buf fflib.Buffer
if j == nil {
buf.WriteString("null")
return buf.Bytes(), nil
}
err := j.MarshalJSONBuf(&buf)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
...
// UnmarshalJSON umarshall json - ffjson template
func (j *LargeStruct) UnmarshalJSON(input []byte) error {
fs := fflib.NewFFLexer(input)
return j.UnmarshalJSONFFLexer(fs, fflib.FFParse_map_start)
}
And here’s an example of marshalling/unmarshalling methods for large JSON objects:
// Benchmark large object marshal method from ffjson package
func BenchmarkFfJsonMarshalLarge(b *testing.B) {
var l int64
for i := 0; i < b.N; i++ {
data, err := ffjson.MarshalFast(&largeData)
if err != nil {
b.Error(err)
}
l = int64(len(data))
}
b.SetBytes(l)
}
// Benchmark large object marshal method with pool from ffjson package
func BenchmarkFfJsonMarshalLargeWithPool(b *testing.B) {
var l int64
for i := 0; i < b.N; i++ {
data, err := ffjson.MarshalFast(&largeData)
if err != nil {
b.Error(err)
}
l = int64(len(data))
ffjson.Pool(data)
}
b.SetBytes(l)
}
// Benchmark concurrent large object marshal method from ffjson package
func BenchmarkFfJsonMarshalLargeParallel(b *testing.B) {
var l int64
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
data, err := ffjson.MarshalFast(&largeData)
if err != nil {
b.Error(err)
}
l = int64(len(data))
}
})
b.SetBytes(l)
}
// Benchmark concurrent large object marshal method with pool from ffjson package
func BenchmarkFfJsonMarshalLargeWithPoolParallel(b *testing.B) {
var l int64
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
data, err := ffjson.MarshalFast(&largeData)
if err != nil {
b.Error(err)
}
l = int64(len(data))
ffjson.Pool(data)
}
})
b.SetBytes(l)
}
// Benchmark large object unmarshal method from ffjson package
func BenchmarkFfJsonUnmarshalLarge(b *testing.B) {
b.SetBytes(int64(len(largeStructString)))
for i := 0; i < b.N; i++ {
var s LargeStruct
if err := ffjson.UnmarshalFast(largeStructString, &s); err != nil {
b.Error(err)
}
}
}
easyjson
This package aims to keep generated Go code simple enough so that it can be easily optimized or fixed. Another goal is to allow users to customize the generated code by providing options unavailable with the standard encoding/json package.
Add this line to generate code:
easyjson -all <filename>.go
Including -all generates a marshaller and unmarshaller for all Go structures in the <filename>.go
file. Here’s an example of statically generated code:
// MarshalJSON supports json.Marshaler interface
func (v LargeStruct) MarshalJSON() ([]byte, error) {
w := jwriter.Writer{}
easyjson794297d0EncodeGitlabYalantisComJsonEncodingBenchmarkEasyjson10(&w, v)
return w.Buffer.BuildBytes(), w.Error
}
// UnmarshalJSON supports json.Unmarshaler interface
func (v *LargeStruct) UnmarshalJSON(data []byte) error {
r := jlexer.Lexer{Data: data}
easyjson794297d0DecodeGitlabYalantisComJsonEncodingBenchmarkEasyjson10(&r, v)
return r.Error()
}
And here are examples of benchmark methods for marshalling and unmarshalling large JSON objects.
// Benchmark large object marshal method from easyjson package
func BenchmarkEasyJsonMarshalLarge(b *testing.B) {
var l int64
for i := 0; i < b.N; i++ {
data, err := largeData.MarshalJSON()
if err != nil {
b.Error(err)
}
l = int64(len(data))
}
b.SetBytes(l)
}
// Benchmark concurrent large object marshal method from easyjson package
func BenchmarkEasyJsonMarshalLargeParallel(b *testing.B) {
b.SetBytes(int64(len(largeStructString)))
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if _, err := largeData.MarshalJSON(); err != nil {
b.Error(err)
}
}
})
}
// Benchmark large object unmarshal method from easyjson package
func BenchmarkEasyJsonUnmarshalLarge(b *testing.B) {
b.SetBytes(int64(len(largeStructString)))
for i := 0; i < b.N; i++ {
var s LargeStruct
if err := s.UnmarshalJSON(largeStructString); err != nil {
b.Error(err)
}
}
}
The above-mentioned packages implement binary encoders that generate static code for each component. This speeds up serialization.
fastjson
Another useful technique that facilitates encoding and decoding is direct string splitting. The following approach doesn’t implement marshalling and unmarshalling. It just performs functions for working with string variables in JSON format and proves itself to be a good solution for marshalling.
This package parses arbitrary JSON without code generation, schema, and reflection. It quickly extracts part of the original JSON with Value.Get(...).MarshalTo
and modifies it with the Del
and Set
functions. It can parse arrays containing values with distinct types – for example, it easily parses the following JSON array: [458, "foo", [155], {"a": "b"}, null].
Here’s an example of a benchmark for parsing large objects:
// Benchmark large object parse method from fastjson package
func BenchmarkFastJsonParseLarge(b *testing.B) {
b.SetBytes(int64(len(xlStructString)))
for i := 0; i < b.N; i++ {
if _, err := fastjson.Parse(string(largeStructString)); err != nil {
b.Error(err)
}
}
}
But it’s unable to parse JSON from io. Reader. For speeding up JSON parsing in Golang from a string, you should use the Scanner type.
json-iterator/go
Just like the standard package, this one is based on reflection, but it claims to have better performance and speed.
It doesn’t require code generation; just import json-iterator/go
in place of the standard package. Below, you’ll find an example of a benchmark method for marshalling and unmarshalling large JSON objects.
// Benchmark large object marshal method from jsoniter package
func BenchmarkJsonIterMarshalLarge(b *testing.B) {
var l int64
for i := 0; i < b.N; i++ {
data, err := jsoniter.Marshal(&largeData)
if err != nil {
b.Error(err)
}
l = int64(len(data))
}
b.SetBytes(l)
}
// Benchmark concurrent large object marshal method from jsoniter package
func BenchmarkJsonIterMarshalLargeParallel(b *testing.B) {
var l int64
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
data, err := jsoniter.Marshal(&largeData)
if err != nil {
b.Error(err)
}
l = int64(len(data))
}
})
b.SetBytes(l)
}
// Benchmark large object unmarshal method from jsoniter package
func BenchmarkJsonIterUnmarshalLarge(b *testing.B) {
b.SetBytes(int64(len(largeStructString)))
for i := 0; i < b.N; i++ {
var s LargeStruct
err := jsoniter.Unmarshal(largeStructString, &s)
if err != nil {
b.Error(err)
}
}
}
Comparing libraries
First, let’s conduct a general analysis of the packages. Both ffjson and easyjson are developed at the same pace and don’t have official releases. Json-iterator/go is being developed intensively, and its creators quickly resolve issues. Also, this package has quickly gained strong support from the developer community.
Despite its young age, valyala/fastjson has gained trust in the developer community. To date, there’s a small list of issues, most of which have been quickly resolved.
The table below sums up the general characteristics of these packages. It lists the current versions of the packages or their last common hashes in case the package has no official releases.
The next step of our comparison is speed testing. For this, we used Go 1.12.6. Using the code above, we’ll check how fast the standard package and four alternative solutions perform Go JSON decoding and encoding.
Read also: Node.js vs Go: Which Is Better for Backend Web Development?
For more precise results, we performed benchmark tests with three types of objects:
-
Small objects (up to 512 KB)
-
Large objects (from 1 to 10 MB)
-
Extra large objects (larger than 10 MB).
Here are the results of our benchmark tests:
To analyze the results of benchmark tests, we’ve created a bar chart that shows the speed of encoding and decoding JSON objects and the memory allocation for the encoding/json, json-iterator/go, ffjson, and easyjson packages.
Here are the memory allocation indicators:
Here’s we try to define the fastest JSON encoder/decoder:
Let’s compare the packages that use reflection for encoding/decoding: encoding/json and json-iterator/go. The standard package performs encoding faster. If we talk about how to speed up the decoding scanner, json-iterator/go performs four times faster than the standard library.
Next are easyjson and ffjson, which use static code generation. The benchmark tests showed that easyjson works 1.5 to 3 times faster than fastjson for both encoding and decoding. The results also showed 3 times faster parallel encoding/decoding in comparison with other packages.
Such high results are reached by the effective use of a buffer pool, which divides large chunks of data into small portions for their further use with sync.Pool()
.
The results of fastjson showed that the parsing method works 3,600 times faster with small objects than with other packages. But on the other hand, its speed decreases as the object size grows. So the speed of encoding large objects is 2 to 3 times slower than others. This is explained by the time-consuming process of text parsing.
Bottom line
Our benchmark tests let us make the following conclusions:
-
Encoding/json is a good solution for working with small objects that have no need to withstand high load.
-
If you need to marshall millions of objects with a similar structure, you can use packages with static code generation. Binary serialization requires two to four times more RAM for handling data compared with other methods.
-
The parsing method used in fastjson boasts exceptional performance, but it can’t decode JSON into objects; it just creates a fieldset. You can get access to these fields using their keys. The fastjson package can be used when you need to check whether a field exists or get the value while bypassing the decoding process.
As you can see, the choice of marshalling/unmarshalling method heavily depends on the type of data you’re going to work with. If you wonder what the best solution is for your project or want to find Go developers for your project, you can always write us. We’ll be glad to help you with Golang development.
Ten articles before and after
How to Deploy Amin Panel Using QOR Golang SDK: Full Guide With Code
Best Tools and Main Reasons to Monitor Go Application Performance
Which Javascript Frameworks to Choose in 2021
Using RxSwift for Reactive Programming in Swift
How to Ensure Efficient Real-Time Big Data Analytics
How to Use GitLab Merge Requests for Code Review
How to Create a Restful API: Your Guide to Making a Developer-Friendly API
Practical Tips on Adding Push Notifications to iOS or Android Apps