こんにちは。Tomoyuki(@tomoyuki65)です。
Go言語(Golang)のバージョン1.27では、JSONをより正確かつ柔軟に扱いやすくするために再設計された「encoding/json/v2」が追加されました。
この記事では、そんなGo言語の「encoding/json/v2」パッケージについてご紹介します。
Go言語(Golang)1.27で登場した「encoding/json/v2」とは?特徴や従来との違い・使い方を解説
「encoding/json/v2」とは?
Go言語の標準ライブラリには、JSONを扱うためのパッケージとして「encoding/json」があります。
そんな従来の「encoding/json」との互換性を維持しつつ、JSON仕様への準拠やカスタマイズ性、そしてパフォーマンス改善などを目指して再設計されたのが「encoding/json/v2」です。
移行の必要性は?
Go1.27からは、従来の「encoding/json」についても内部的にはv2の実装を利用するようになっていますが、無理に移行する必要性はないため、必要になった場合に段階的にv2を使うのが現実的です。
ただし、v2ではJSONの扱いがより厳格・標準準拠寄りになっているため、単純にimportを書き換えるのは危険なので注意して下さい。
| 項目 | encoding/json(v1) |
encoding/json/v2 |
|---|---|---|
| 不正なUTF-8 | 置換して処理 | エラー |
| JSONの重複キー | 許容 | エラー |
| nil slice | null |
[] |
| nil map | null |
{} |
| structフィールド名のマッチ | 大文字小文字を区別しない | 基本的に区別する |
| Options | 限定的 | 柔軟に指定可能 |
io.Reader / io.Writer |
Encoder / Decoder | MarshalRead / MarshalWrite 等が使える |
UnmarshalとMarshalのメソッドでオプション指定が可能になった
v2ではJSONのデコード(Unmarshal)・エンコード(Marshal)の際にオプション設定で細かく制御できる設計になり、「json.Unmarshal(data, &v, opts...)」や「json.Marshal(v, opts...)」のようにそれぞれオプションを指定できます。
例えば従来の場合は、デコード時にunknown fieldがあっても無視され、正常終了していました。
package main
import (
"encoding/json"
"fmt"
)
// ユーザー構造体の定義
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
// JSON形式のデータ
data := []byte(`{
"name": "田中",
"age": 21,
"admin": true
}`)
// デコード処理
var u User
if err := json.Unmarshal(data, &u); err != nil {
fmt.Printf("エラーメッセージ:%v\n", err)
return
}
// フィールド名付きでログ出力
fmt.Printf("%+v\n", u)
}
実行結果
{Name:田中 Age:21}
v2ではオプション「json.RejectUnknownMembers(true)」を設定することでunknown fieldがあった場合にエラーにできます。
例えば従来の場合は、デコード時にunknown fieldがあっても無視され、正常終了していました。
package main
import (
"encoding/json/v2"
"fmt"
)
// ユーザー構造体の定義
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
// JSON形式のデータ
data := []byte(`{
"name": "田中",
"age": 21,
"admin": true
}`)
// デコード処理
var u User
if err := json.Unmarshal(data, &u, json.RejectUnknownMembers(true)); err != nil {
fmt.Printf("エラーメッセージ:%v\n", err)
return
}
// フィールド名付きでログ出力
fmt.Printf("%+v\n", u)
}
実行結果
エラーメッセージ:json: cannot unmarshal JSON string into Go main.User: unknown object member name "admin"
このようにデコードやエンコード時にオプション設定を追加することで、細かい制御が可能になっています。
尚、オプションに設定できる値は以下の通りです。
| オプション | Marshal | Unmarshal | 用途 |
|---|---|---|---|
DefaultOptionsV2() |
✓ | ✓ | v2のデフォルトオプションを取得 |
Deterministic(bool) |
✓ | — | 決定論的なJSON出力 |
FormatNilMapAsNull(bool) |
✓ | — | nil mapをnullとして出力 |
FormatNilSliceAsNull(bool) |
✓ | — | nil sliceをnullとして出力 |
JoinOptions(...Options) |
✓ | ✓ | 複数のOptionsを合成 |
MatchCaseInsensitiveNames(bool) |
— | ✓ | JSON名とGoフィールド名を大小文字を無視してマッチ |
OmitZeroStructFields(bool) |
✓ | — | structのzero値フィールドを省略 |
RejectUnknownMembers(bool) |
— | ✓ | 未知のJSONメンバーをエラーにする |
StringifyNumbers(bool) |
✓ | ✓ | 数値をJSON文字列として扱う |
WithMarshalers(*Marshalers) |
✓ | — | 独自のMarshal処理を登録 |
WithUnmarshalers(*Unmarshalers) |
— | ✓ | 独自のUnmarshal処理を登録 |
※複数オプションの組み合わせも可能です。
構造体に付与できるstruct tagが整理・拡張された
Go1.27では構造体に付与できるstruct tagが以下のように整理・拡張されています。
| struct tag | 意味 |
|---|---|
json:"name" |
JSON名を指定 |
json:"-" |
JSONから除外 |
json:",omitzero" |
Goのzero valueなら省略 |
json:",omitempty" |
JSONとしてemptyなら省略 |
json:",string" |
JSON numberをstringで表現 |
json:",case:ignore" |
case / - / _ を無視してmatch |
json:",case:strict" |
case-sensitiveでmatch |
json:",embed" |
フィールドを親objectへ展開 |
omitzeroの例(zero valueなら省略)
package main
import (
"encoding/json/v2"
"fmt"
)
// ユーザー構造体の定義
type User struct {
Name string `json:"name,omitzero"`
Age int `json:"age,omitzero"`
Email string `json:"email,omitzero"`
}
func main() {
// ユーザー設定
user := User{
Name: "Alice",
Age: 0,
Email: "",
}
// エンコード処理
b, err := json.Marshal(user)
if err != nil {
panic(err)
}
// []byte型の値をstring型に変換してログ出力
fmt.Println(string(b))
}
実行結果
{"name":"Alice"}
case:ignoreの例(case / – / _ を無視してmatch)
package main
import (
"encoding/json/v2"
"fmt"
)
// ユーザー構造体の定義
type User struct {
Name string `json:"userName,case:ignore"`
}
func main() {
// インプットデータ
inputs := []string{
`{"userName":"Alice"}`,
`{"username":"Bob"}`,
`{"USERNAME":"Carol"}`,
`{"user_name":"Dave"}`,
`{"user-name":"Eve"}`,
}
for _, input := range inputs {
var user User
// デコード処理
if err := json.Unmarshal([]byte(input), &user); err != nil {
fmt.Printf("エラーメッセージ:%v\n", err)
continue
}
// 20文字分の幅を確保して左寄せし、結果を引用符付きでログ出力
fmt.Printf("%-20s -> %q\n", input, user.Name)
}
}
実行結果
{"userName":"Alice"} -> "Alice"
{"username":"Bob"} -> "Bob"
{"USERNAME":"Carol"} -> "Carol"
{"user_name":"Dave"} -> "Dave"
{"user-name":"Eve"} -> "Eve"
case:strictの例(case-sensitiveでmatch)
package main
import (
"encoding/json/v2"
"fmt"
)
// ユーザー構造体の定義
type User struct {
Name string `json:"userName,case:strict"`
}
func main() {
// インプットデータ
inputs := []string{
`{"userName":"Alice"}`,
`{"username":"Bob"}`,
`{"USERNAME":"Carol"}`,
`{"user_name":"Dave"}`,
`{"user-name":"Eve"}`,
}
for _, input := range inputs {
var user User
// デコード処理
if err := json.Unmarshal([]byte(input), &user); err != nil {
fmt.Printf("エラーメッセージ:%v\n", err)
continue
}
// 20文字分の幅を確保して左寄せし、結果を引用符付きでログ出力
fmt.Printf("%-20s -> %q\n", input, user.Name)
}
}
実行結果
{"userName":"Alice"} -> "Alice"
{"username":"Bob"} -> ""
{"USERNAME":"Carol"} -> ""
{"user_name":"Dave"} -> ""
{"user-name":"Eve"} -> ""
尚、v2のデフォルトでは「case:strict」と同じ動作になるため、全体のオプションとして「json.MatchCaseInsensitiveNames(true)」を設定した場合に利用する可能性があります。
embedの例(フィールドを親objectへ展開)
package main
import (
"encoding/json/v2"
"fmt"
)
// 住所構造体の定義
type Address struct {
City string `json:"city"`
Zip string `json:"zip"`
}
// ユーザー構造体の定義
type User struct {
Name string `json:"name"`
Address Address `json:",embed"`
}
func main() {
// JSON形式のデータ
data := []byte(`{
"name": "佐藤",
"city": "東京",
"zip": "100-0001"
}`)
// デコード処理
var u User
if err := json.Unmarshal(data, &u); err != nil {
fmt.Printf("エラーメッセージ:%v\n", err)
return
}
// フィールド名付きでログ出力
fmt.Printf("%+v\n", u)
}
実行結果
{Name:佐藤 Address:{City:東京 Zip:100-0001}}
JSONがストリーム処理(データを少しずつ読み書きする方式)として扱いやすくなった
v2ではUnmarshalReadやMarshalWriteといった新しいメソッドが追加され、[]byteを介さず、io.Readerやio.Writerと組み合わせてストリーム処理(データを少しずつ読み書きする方式)がしやすくなりました。
また、JSON構文そのものを扱うための「encoding/json/jsontext」パッケージも追加され、JSONの構造を読み取りながら、必要な部分だけを処理するような実装もしやすくなっています。
| やりたいこと | API |
|---|---|
Go → []byte |
json.Marshal |
[]byte → Go |
json.Unmarshal |
Go → io.Writer |
json.MarshalWrite |
io.Reader → Go |
json.UnmarshalRead |
| Go → JSONストリーム | json.MarshalEncode |
| JSONストリーム → Go | json.UnmarshalDecode |
| JSON構文を直接読む | jsontext.Decoder |
| JSON構文を直接書く | jsontext.Encoder |
デコード(Unmarshal)が大きく高速化
上記のストリーム処理の話にも繋がりますが、JSONを効率的に解析できるようになったことで、デコード(Unmarshal)のパフォーマンスが向上し、ベンチマークによっては最大10倍程度の改善を確認されるなど、大きく高速化しています。
尚、Go1.27では従来の「encoding/json」についても内部的にはv2の実装を利用するようになっているため、高速化だけを目的として「encoding/json/v2」に置き換える必要はないです。
最後に
今回はGo言語の「encoding/json/v2」パッケージについてご紹介しました。
無理に移行する必要性はありませんが、新しいメソッドや「encoding/json/jsontext」パッケージの追加により、ストリーム処理で色々できることが増えているため、必要に応じて利用することになると思います。
Go1.27で追加された「encoding/json/v2」パッケージについてキャッチアップしたい方は、ぜひ参考にしてみて下さい。




コメント