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/v2とencoding/json/jsontextが標準ライブラリ入りする(GOEXPERIMENT不要)- 既存の
encoding/json(v1)の内部実装が、v2エンジンをバックエンドとして全面的に書き換えられた GOEXPERIMENTの意味が反転した- Go1.25/1.26:
GOEXPERIMENT=jsonv2で有効化 - Go1.27:デフォルト有効。旧実装に戻したいときのみ
GOEXPERIMENT=nojsonv2(将来削除予定)
- Go1.25/1.26:
- experimentalで使えた
formatタグが標準から外れた encoding/json/v2はv1よりも厳格で相互運用性の高いデフォルト挙動を採用した
encoding/jsonパッケージ自体は非推奨化・削除されません。従来のAPI(json.Marshalやjson.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 { |
この 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 []string は null、b := []string{} は [])、レスポンスを常に [] にしたいなら明示的に空スライスで初期化する必要がありました。v2ではこの初期化漏れそのものが不要になります。厳密にはREST APIのレスポンス互換に影響があるため、後方互換性のために FormatNilSliceAsNull() / FormatNilMapAsNull() のオプションが用意されています。
time.Duration はv1では内部表現のナノ秒整数がそのままJSON数値になっていました(5 * time.Second は 5000000000)。この表現を続けるべきか文字列("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は特に気をつける必要あり