この記事は、サイバーエージェント26卒内定者 Advent Calendarの17日目の記事です。
はじめに
Go 1.25にて、標準ライブラリの encoding/json の最新版である encoding/json/v2 が実験的に導入されました。
私自身、これまでは既存の標準ライブラリを使うことが多かったのですが、内定者アルバイト先で go-json が採用されていたことをきっかけに、他のライブラリについても関心を持つようになりました。
そこで本記事では、標準ライブラリ(v1/v2)をはじめとする主要なJSONエンコーダ/デコーダについて、サンプルコードを交えて紹介するとともに、ベンチマークによる性能比較を行います。
TL; DR
- jsonのMarshal, Unmarshalに関するライブラリを紹介し、ベンチマークを行いました
-
go-jsonがjsonのデコードが最も高速で、エンコードでも高性能を発揮し、パフォーマンス的に最も優れており、encoding/jsonからの置き換えが可能であり、より高速な処理が必要な場合は最適だと思います - 標準ライブラリの
encoding/jsonは、Unmarshalにおいて最もパフォーマンスが悪く、encoding/json/v2ではパフォーマンスが向上していました -
easyjsonは自動生成されたコードでjsonのエンコード・デコードを行うため、リフレクションを使用せずメモリ効率が最も良かったです - Marshalでは、
segmentio/encoding/jsonが最も高速という結果になり、エンコード処理で高いパフォーマンスが必要な場合、最適かと思います。こちらもencoding/jsonと置き換え可能です - 大規模なjsonデータの特定の値の取得、更新を最速で行いたい場合は
gjsonとsjsonです
主なjsonエンコーダ/デコーダ
1. encoding/json
- Goの標準ライブラリ
- 標準ライブラリであり公式がサポート
- 依存関係がない
- リフレクションを多用しており、他のライブラリに比べパフォーマンス面が課題
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
u := User{ID: 1, Name: "Gopher"}
// Marshal
b, _ := json.Marshal(u)
fmt.Println("Marshal:", string(b))
// Unmarshal
var u2 User
json.Unmarshal(b, &u2)
fmt.Printf("Unmarshal: %+v\n", u2)
}
2. encoding/json/v2
- Go 1.25で実験的に追加されている標準ライブラリ
-
GOEXPERIMENT=jsonv2を有効にすることで使用可能 - アロケーションのパフォーマンスの大幅向上
package main
import (
"fmt"
jsonv2 "encoding/json/v2"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
u := User{ID: 1, Name: "Gopher"}
// Marshal
b, _ := jsonv2.Marshal(u)
fmt.Println("Marshal:", string(b))
// Unmarshal
var u2 User
jsonv2.Unmarshal(b, &u2)
fmt.Printf("Unmarshal: %+v\n", u2)
}
3. go-json
- Go の encoding/json と互換性のある高速 JSON エンコーダ/デコーダ
- encoding/jsonのメソッドをそのまま置き換えることができる
- 構造体のフィールドを型安全に動的にフィルタリングできる
- リフレクションの利用の制限、
go-reflectでリフレクションをゼロアロケーションで利用可能などパフォーマンスで最適化 - スター数:3.6k
package main
import (
"fmt"
json "github.com/goccy/go-json"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
u := User{ID: 1, Name: "Gopher"}
// Marshal
b, _ := json.Marshal(u)
fmt.Println(string(b))
// Unmarshal
var u2 User
json.Unmarshal(b, &u2)
}
4. json-iterator
- リフレクションのキャッシュ化などで高速化
- 型ごとのエンコーダ/デコーダのキャッシング
- スター数:13.9k
package main
import (
"fmt"
jsoniter "github.com/json-iterator/go"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
var json = jsoniter.ConfigCompatibleWithStandardLibrary
u := User{ID: 1, Name: "Gopher"}
// Marshal
b, _ := json.Marshal(u)
fmt.Println("Marshal:", string(b))
// Unmarshal
var u2 User
json.Unmarshal(b, &u2)
fmt.Printf("Unmarshal: %+v\n", u2)
}
5. gojay
- リフレクションを使用しないため、高速
- ゼロアロケーション
- 構造体に対し、メソッドを定義する必要がある
- コードの記述量が多くメンテナンスコストがかかる
- スター数:2.1k
package main
import (
"fmt"
"github.com/francoispqt/gojay"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func (u *User) MarshalJSONObject(enc *gojay.Encoder) {
enc.IntKey("id", u.ID)
enc.StringKey("name", u.Name)
}
func (u *User) IsNil() bool {
return u == nil
}
func (u *User) UnmarshalJSONObject(dec *gojay.Decoder, key string) error {
switch key {
case "id":
return dec.Int(&u.ID)
case "name":
return dec.String(&u.Name)
}
return nil
}
func (u *User) NKeys() int {
return 2
}
func main() {
u := &User{ID: 1, Name: "Gopher"}
// Marshal
b, _ := gojay.Marshal(u)
fmt.Println("Marshal:", string(b))
// Unmarshal
u2 := &User{}
gojay.Unmarshal(b, u2)
fmt.Printf("Unmarshal: %+v\n", u2)
}
6. sonic
- JIT (just-in-time compiling) と SIMD (single-instruction-multiple-data) によって高速化
- JSON全体を構造体に変換せず、特定のキーだけを取得・変更する
sonic.Getを提供 - Go1.24.0 はサポートしていない→ issue
- スター数:9k
package main
import (
"fmt"
"github.com/bytedance/sonic"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
u := User{ID: 1, Name: "Gopher"}
// Marshal
b, _ := sonic.Marshal(u)
fmt.Println("Marshal:", string(b))
// Unmarshal
var u2 User
sonic.Unmarshal(b, &u2)
fmt.Printf("Unmarshal: %+v\n", u2)
}
7. sonnet
- Go標準ライブラリと完全に互換性
- 標準ライブラリの"encoding/json" に依存していない
- Pure Goで実現されている
- スター数:301
package main
import (
"fmt"
"github.com/sugawarayuuta/sonnet"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
u := User{ID: 1, Name: "Gopher"}
// Marshal
b, _ := sonnet.Marshal(u)
fmt.Println("Marshal:", string(b))
// Unmarshal
var u2 User
sonnet.Unmarshal(b, &u2)
fmt.Printf("Unmarshal: %+v\n", u2)
}
8. segmentio
- 標準ライブラリと完全な互換性
- 不要な動的メモリ割り当てをなくし、reflect packageへの呼び出しがほとんど発生せずメモリ効率が良い
- 内部でunsafeパッケージを使用
- スター数:1k
package main
import (
"fmt"
json "github.com/segmentio/encoding/json"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
u := User{ID: 1, Name: "Gopher"}
// Marshal
b, _ := json.Marshal(u)
fmt.Println("Marshal:", string(b))
// Unmarshal
var u2 User
json.Unmarshal(b, &u2)
fmt.Printf("Unmarshal: %+v\n", u2)
}
9. easyjson
- 構造体を定義し、構造体に適切なMarshaler・Unmarshaler関数を自動生成
- スネークケース形式の名前の生成やデフォルトの動作の有効化など
- オブジェクトキーは大文字と小文字が区別される
- スター数:4.8k
- typesを定義
-
easyjson -all models/types.goで自動生成 - 生成されたメソッドをimportして使用する
easyjson_demo/
├── models/
│ ├── types.go # 構造体定義
│ └── types_easyjson.go # 自動生成されたコード
├── main.go # メインプログラム
├── go.mod
└── go.sum
package models
//easyjson:json
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
// 自動生成のコードのため省略しています
// MarshalJSON supports json.Marshaler interface
func (v User) MarshalJSON() ([]byte, error) {
// UnmarshalJSON supports json.Unmarshaler interface
func (v *User) UnmarshalJSON(data []byte) error {
package main
import (
"easyjson_demo/models"
"fmt"
)
func main() {
user := models.User{
ID: 1,
Name: "Gopher",
}
// MarshalJSON
b, err := user.MarshalJSON()
if err != nil {
panic(err)
}
fmt.Println("Marshal:", string(b))
// UnmarshalJSON
var user2 models.User
err = user2.UnmarshalJSON([]byte(string(b)))
if err != nil {
panic(err)
}
fmt.Printf("Unmarshal: %+v\n", user2)
}
10 gjson
- 指定されたパスからJSONの中の特定の値を高速に取り出すことができる
- JSONの特定の値の抽出に優れており、Unmarshalはできない
- 関数修飾子とパスチェーンのサポート
-
String(),Int(),Bool()で取り出した値のキャストが容易 - エンコードはサポートしていない
- スター数:15.4k
package main
import (
"encoding/json"
"fmt"
"github.com/tidwall/gjson"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
u := User{ID: 1, Name: "Gopher"}
// Marshal (encoding/jsonを使用)
b, _ := json.Marshal(u)
fmt.Println("Marshal:", string(b))
// gjson.Get で値を取得
result := gjson.Parse(string(b))
id := result.Get("id").Int()
name := result.Get("name").String()
// Unmarshal相当(手動で構造体に設定)
u2 := User{
ID: int(id),
Name: name,
}
fmt.Printf("Unmarshal: %+v\n", u2)
}
11. sjson
- gjsonと同じ開発者
- gjsonに対し、エンコードに特化
- 構造体の定義が不要で、JSONの一部に高速で書き込みが可能
- スター数:2.7k
package main
import "github.com/tidwall/sjson"
const jsonString = `{"id":1,"name":"Anderson"}`
func main() {
// デコード・構造体の定義不要で書き込める
value, _ := sjson.Set(jsonString, "name", "Gopher")
println(value)
}
12. fastjson
- パーサー
- input JSON の解析は1回のみで、複数のフィールドにアクセスするケースではgjsonより優れている
- リフレクションを使わず、アロケーションを最小限に抑える設計
- JSON内の 複数の異なるフィールド にアクセスする必要がある場合に優れている
- スター数:2.4k
package main
import (
"encoding/json"
"fmt"
"github.com/valyala/fastjson"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
u := User{ID: 1, Name: "Gopher"}
// Marshal (encoding/jsonを使用)
b, _ := json.Marshal(u)
fmt.Println("Marshal:", string(b))
// Parse
var p fastjson.Parser
v, _ := p.Parse(string(b))
// Get values
id := v.GetInt("id")
name := string(v.GetStringBytes("name"))
// Unmarshal相当(手動で構造体に設定)
u2 := User{
ID: id,
Name: name,
}
fmt.Printf("Unmarshal: %+v\n", u2)
}
比較
- go-json/benchmarksに今回紹介したライブラリを追加し、合計12のツールに対してベンチマークを行いました
- パーサーのgjson, fastjsonはUnmarshalのみ、書き込みのsjsonはEncodeのみ実行しました
※gjsonは、Unmarshalの機能を持っていないため、json→構造体になるように全ての値に対して取得する処理を行っています
テストデータ
- Small Struct(小規模データ)
- サイズ:143 bytes
- Medium Struct(中規模データ)
- サイズ:2,071 bytes
- Large Struce(大規模データ)
- サイズ:28,123 bytes
テスト項目
- Unmarshal
- json→構造体
- Encode
- 構造体 → JSON(キャッシュなし)
ベンチマーク環境
- OS: darwin (macOS)
- アーキテクチャ: arm64
- CPU: Apple M4 Max
- Go バージョン: 1.25.5
結果
Small Struct
Unmarshal
| Rank | ライブラリ | 処理時間 (ns/op) | Memory (B/op) | Allocation (allocs/op) |
|---|---|---|---|---|
| 1 | go-json | 178.8 | 256 | 2 |
| 2 | easyjson | 190.4 | 0 | 0 |
| 3 | gojay | 217.6 | 256 | 2 |
| 4 | sonnet | 337.5 | 320 | 7 |
| 5 | json-iterator | 375.3 | 208 | 13 |
| 6 | segmentio | 399.3 | 192 | 5 |
| 7 | sonic | 514.2 | 420 | 4 |
| 8 | encoding/json/v2 | 594.5 | 112 | 1 |
| 9 | gjson | 689.5 | 96 | 6 |
| 10 | fastjson | 791.1 | 3560 | 11 |
| 11 | encoding/json | 1041.0 | 416 | 9 |
Encode
| Rank | ライブラリ | 処理時間 (ns/op) | Memory (B/op) | Allocation (allocs/op) |
|---|---|---|---|---|
| 1 | segmentio | 126.8 | 256 | 2 |
| 2 | go-json | 199.9 | 432 | 2 |
| 3 | encoding/json | 281.2 | 256 | 2 |
| 4 | encoding/json/v2 | 285.3 | 256 | 2 |
| 5 | sonic | 289.5 | 278 | 3 |
| 6 | easyjson | 304.9 | 720 | 4 |
| 7 | sonnet | 321.7 | 600 | 6 |
| 8 | gojay | 326.1 | 624 | 2 |
| 9 | json-iterator | 419.2 | 264 | 3 |
| 10 | sjson | 1602.0 | 2922 | 48 |
Medium Struct
Unmarshal
| Rank | ライブラリ | 処理時間 (ns/op) | Memory (B/op) | Allocation (allocs/op) |
|---|---|---|---|---|
| 1 | go-json | 1404.0 | 2426 | 8 |
| 2 | gojay | 1937.0 | 2425 | 8 |
| 3 | sonic | 2004.0 | 3044 | 13 |
| 4 | easyjson | 2439.0 | 64 | 4 |
| 5 | segmentio | 2535.0 | 272 | 9 |
| 6 | json-iterator | 2936.0 | 768 | 82 |
| 7 | sonnet | 3046.0 | 336 | 11 |
| 8 | encoding/json/v2 | 3376.0 | 72 | 4 |
| 9 | fastjson | 4365.0 | 18648 | 54 |
| 10 | gjson | 5248.0 | 488 | 22 |
| 11 | encoding/json | 8744.0 | 624 | 18 |
Encode
| Rank | ライブラリ | 処理時間 (ns/op) | Memory (B/op) | Allocation (allocs/op) |
|---|---|---|---|---|
| 1 | segmentio | 397.5 | 609 | 15 |
| 2 | easyjson | 459.0 | 896 | 4 |
| 3 | go-json | 525.5 | 864 | 15 |
| 4 | sonnet | 600.0 | 952 | 19 |
| 5 | gojay | 764.3 | 800 | 15 |
| 6 | encoding/json/v2 | 839.3 | 608 | 15 |
| 7 | encoding/json | 864.8 | 608 | 15 |
| 8 | sonic | 913.3 | 646 | 16 |
| 9 | json-iterator | 1129.0 | 616 | 16 |
| 10 | sjson | 1149.0 | 2290 | 24 |
Large Struct
Unmarshal
| Rank | ライブラリ | 処理時間 (ns/op) | Memory (B/op) | Allocation (allocs/op) |
|---|---|---|---|---|
| 1 | go-json | 19086.0 | 30530 | 67 |
| 2 | gojay | 25176.0 | 31005 | 77 |
| 3 | sonic | 25410.0 | 32851 | 71 |
| 4 | sonnet | 28210.0 | 3670 | 131 |
| 5 | encoding/json/v2 | 37170.0 | 32 | 1 |
| 6 | easyjson | 39105.0 | 0 | 0 |
| 7 | segmentio | 49599.0 | 4128 | 132 |
| 8 | json-iterator | 55895.0 | 16359 | 1268 |
| 9 | fastjson | 57889.0 | 275241 | 540 |
| 10 | gjson | 63889.0 | 29550 | 72 |
| 11 | encoding/json | 122987.0 | 4432 | 147 |
Encode
| Rank | ライブラリ | 処理時間 (ns/op) | Memory (B/op) | Allocation (allocs/op) |
|---|---|---|---|---|
| 1 | segmentio | 8610.0 | 14106 | 319 |
| 2 | sonnet | 9655.0 | 14404 | 323 |
| 3 | easyjson | 9943.0 | 14793 | 326 |
| 4 | go-json | 11386.0 | 20380 | 319 |
| 5 | gojay | 15101.0 | 27505 | 323 |
| 6 | encoding/json/v2 | 15877.0 | 14124 | 319 |
| 7 | encoding/json | 16290.0 | 14110 | 319 |
| 8 | sjson | 17633.0 | 36110 | 333 |
| 9 | sonic | 18345.0 | 14717 | 320 |
| 10 | json-iterator | 19771.0 | 14117 | 320 |
Unmarshal(json→構造体)の性能はgo-jsonがSmall, Medium, Largeの全てにおいて高速でした。標準ライブラリのencoding/jsonは、Unmarshalにおいて最も遅かったです。一方encoding/json/v2は、Unmarshalでかなり高速化していると言えます。
easyjsonはUnmarshalにおいて、メモリ効率が最も良い結果となりました。これは自動生成されたコードで行い、リフレクションを使用しないことによるものだと思います。
Encode(構造体→json)ではsegmentioが最も高速という結果になりました。
まとめ
jsonのエンコーダ/デコーダのライブラリでも非常に多くあり、それぞれ特徴がありとても興味深かったです。また、標準のパッケージと互換性を大事にしているパッケージが多く使いやすいなと感じました。Webサーバー開発においてパフォーマンスは非常に重要な要件となってくるので、ライブラリの選定もよく考えるべきだと改めて感じました。メソッドの自動生成やパス指定、リフレクションを使わないなどパフォーマンスの最適化で様々なアプローチが取られており調べていて、とても勉強になりました。
今回は各ライブラリの概要だけをまとめましたが、内部実装も追って
また記事に適宜情報などを追加していきます。