PR

Go言語(Golang)1.27で登場した「encoding/json/v2」とは?特徴や従来との違い・使い方を解説

2. 基礎

こんにちは。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」パッケージについてキャッチアップしたい方は、ぜひ参考にしてみて下さい。

 

SubstackでITエンジニア向けニュースレター(メルマガ)を始めました!
こんにちは。Tomoyuki(@tomoyuki65)です。最近はどのSNSもアルゴリズム(どの投稿を誰に表示するかを決める仕組み)中心のタイムライン(投稿が流れてくる画面)になっていて、フォローしている人の投稿が見つけにくく、逆にフォロー...

 

この記事を書いた人
Tomoyuki

SE→ブロガーを経て、現在はSoftware Engineer(Web/Gopher)をしています!

Tomoyukiをフォローする
2. 基礎
スポンサーリンク
Tomoyukiをフォローする

コメント

タイトルとURLをコピーしました