フューチャー技術ブログ

Go1.27リリース連載:encoding/json/v2

The Go gopher was designed by Renee French.

Go 1.27 ブログ連載 の 4 本目です。

はじめに

製造エネルギーサービス事業部の辻です。

この記事では、Go1.27で新たに利用可能になる encoding/json/v2 を取り上げます。

encoding/json/v2 については、昨年のGo1.25連載で後藤さんが Go 1.25リリース連載 encoding/json/v2(experimental) で詳しく取り上げています。当時は GOEXPERIMENT=jsonv2 を有効にして試す実験的機能という位置づけで、omitempty の挙動変化と性能に焦点が当てられていました。本記事では、その1年でどこが変わったかを中心に見ていきます。

Go1.27での変更点サマリ

  • encoding/json/v2encoding/json/jsontext が標準ライブラリ入りする(GOEXPERIMENT 不要)
  • 既存の encoding/json(v1)の内部実装が、v2エンジンをバックエンドとして全面的に書き換えられた
  • GOEXPERIMENT の意味が反転した
    • Go1.25/1.26:GOEXPERIMENT=jsonv2 で有効化
    • Go1.27:デフォルト有効。旧実装に戻したいときのみ GOEXPERIMENT=nojsonv2(将来削除予定)
  • experimentalで使えた format タグが標準から外れた
  • encoding/json/v2 はv1よりも厳格で相互運用性の高いデフォルト挙動を採用した

encoding/jsonパッケージ自体は非推奨化・削除されません。従来のAPI(json.Marshaljson.Unmarshalなど)はそのまま使え、Go1.27へ上げるだけで内部的にv2エンジンに切り替わります。

深堀り1:encoding/jsonの内部実装がv2ベースに置き換わり、GOEXPERIMENTの意味合いが反転した

Go1.25時点は「GOEXPERIMENT=jsonv2 を付けると内部実装が置き換わる」というオプトインでした。Go1.27ではこれが逆転し、何もしなくても encoding/json の内部がv2エンジンで動きます。encoding/jsonパッケージには、旧実装(decode.go等)とv2ベースの新実装(v2_decode.go等)が両方コミットされており、ビルドタグで排他的に切り替わります。挙動が怪しいときは GOEXPERIMENT=nojsonv2 で旧実装に戻し、切り分けられます。

一つ注意点があります。リリースノートには「Marshaling and unmarshaling behavior is preserved, but the exact text of error messages may differ.」とあり、挙動自体は互換が保たれますが、エラーメッセージの文言(err.Error()の文字列)は変わり得るとされています。err.Error() == "..."のような文字列完全一致でテストしているコードは、Go1.27で壊れる可能性があります。エラーは文字列ではなく errors.Is / errors.As で判定するのが、よいプラクティスです。

深堀り2:format タグが標準から外れた

Go1.25 / 1.26のexperimentalでは、フィールドのエンコード表現を構造体タグで指定する format オプションが使えました1。例えば time.Time は本来RFC 3339形式の文字列でエンコードされますが、format:RFC3339 と明示的に書くこともできましたし、Formatメソッドが解釈できる任意のレイアウト文字列(例: format:"2006-01-02")を指定して表現を変えることもできました。

type Event struct {
At time.Time `json:"at,format:RFC3339"` // "2026-07-30T00:00:00Z" のような表現
}

この format タグはGo1.27の標準APIからは外れます(#79071)。Go言語自体にtyped struct tagsを導入する提案(#74472)を見越し、パッケージ内に専用のDSLを持たせない方針への転換です。

深堀り3:デフォルト挙動の変化

encoding/json/v2 APIのデフォルト挙動はv1から複数変わっています。代表的な差分です。

挙動項目 v1 v2 v1に戻すオプション
不正なUTF-8 U+FFFD(�) に置換 エラー jsontext.AllowInvalidUTF8(true)
重複キー 許容(後の値が前の値を置換またはマージ) エラー jsontext.AllowDuplicateNames(true)
フィールド名マッチ 大文字小文字を無視 完全一致のみ MatchCaseInsensitiveNames(true)
nilスライス/マップ null 通常は [] / {} FormatNilSliceAsNull(true) / FormatNilMapAsNull(true)
time.Duration ナノ秒の数値 エラー FormatDurationAsNano(true)

nilスライス/マップが null ではなく [] / {} としてエンコードされるのは、個人的には嬉しいポイントです。v1では、nilスライスと空スライスでエンコード結果が異なり(var a []stringnullb := []string{}[])、レスポンスを常に [] にしたいなら明示的に空スライスで初期化する必要がありました。v2ではこの初期化漏れそのものが不要になります。厳密にはREST APIのレスポンス互換に影響があるため、後方互換性のために FormatNilSliceAsNull() / FormatNilMapAsNull() のオプションが用意されています。

time.Duration はv1では内部表現のナノ秒整数がそのままJSON数値になっていました(5 * time.Second5000000000)。この表現を続けるべきか文字列("5s")にすべきかがissue上で決着しておらず(#71631)、v2では意図的にデフォルト表現が定められていません。そのため何も指定しないとMarshal、Unmarshalともにエラーになります。v1互換のナノ秒整数のままでよい場合はencoding/json.FormatDurationAsNano(true)を指定し、文字列など別の表現にしたい場合は独自型 + MarshalerTo / UnmarshalerFrom で明示的に定義します。

まとめ

  • experimentalからの1年で、GOEXPERIMENT が反転し、v1がv2ベースに再実装された
  • Go1.27へ上げるだけで、既存コードのまま内部的にv2に切り替わる
  • experimentalで使えた format タグは標準から外れる
  • フィールド名マッチの厳格化、nilスライス/マップ、time.Duration は特に気をつける必要あり