はじめに
Go 1.27が2026年8月19日に正式リリースされました。目玉のひとつが標準ライブラリに新しく加わった encoding/json/v2 です1。既存の encoding/json(以下v1)と同じような使い方ができますが、パッケージ名を変えただけの別実装ではなく、デコード時の既定挙動そのものが変わっています。対象読者は、Go でJSONを扱うAPIサーバーやCLIツールを書いているエンジニアです。
本記事では、Go 1.27を実際にインストールして encoding/json と encoding/json/v2 に同じ壊れたJSONを食わせ、挙動の違いをログで比較しました。あわせてUnmarshalの速度差も計測しています。
比較表(v1 と v2、同じ入力での挙動差)
| # | 検証項目 | 入力 |
encoding/json(v1) |
encoding/json/v2(v2) |
|---|---|---|---|---|
| 1 | 重複キー | {"Name":"Alice","Name":"Bob","Age":30} |
エラーなし・後勝ちで Bob が入る |
jsontext: duplicate object member name "Name" でエラー |
| 2 | 不正なUTF-8 | 値に \xff\xfe を含むJSON |
エラーなし・不正バイトを黙って通す |
jsontext: invalid UTF-8 within "/Name" でエラー |
| 3 | Unmarshal速度(10フィールドの構造体) | 同一の73バイトJSON | 1,897 ns/op・128 B/op | 1,540 ns/op・128 B/op(約19%高速) |
検証環境: Go 1.27.0 linux/amd64、go test -bench=. -benchmem。以下、各項目を実際のコードと出力で見ていきます。
1. 重複キーはもう「後勝ち」で通らない
まず、同じキーが2回出てくるJSONをそれぞれのパッケージに食わせてみます。
package main
import (
"fmt"
jsonv1 "encoding/json"
jsonv2 "encoding/json/v2"
)
type Person struct {
Name string
Age int
}
func main() {
dupKeyJSON := []byte(`{"Name":"Alice","Name":"Bob","Age":30}`)
var p1 Person
err1 := jsonv1.Unmarshal(dupKeyJSON, &p1)
fmt.Printf("v1: result=%+v err=%v\n", p1, err1)
var p2 Person
err2 := jsonv2.Unmarshal(dupKeyJSON, &p2)
fmt.Printf("v2: result=%+v err=%v\n", p2, err2)
}
実際にGo 1.27.0で実行した出力です。
v1: result={Name:Bob Age:30} err=<nil>
v2: result={Name:Alice Age:0} err=jsontext: duplicate object member name "Name"
v1は Name が2回来ても文句を言わず、後に書かれた Bob で上書きしてエラーを返しません。呼び出し側は「正常に処理できた」としか判断できず、どちらの値が採用されたかはコードを読むまでわかりません。v2は jsontext: duplicate object member name "Name" というエラーを返し、デコードをその場で止めます。
外部から受け取ったJSONに重複キーが混入するケースは、プロキシでのヘッダー結合やクライアント側のバグ、あるいは意図的な攻撃(後続キーで前のキーの値を上書きさせる)で普通に起こります。v1のまま運用していると、この種のペイロードを検証なしに通してしまいます。
2. 不正なUTF-8も同様にエラーになる
次に、文字列値の中に不正なバイト列(\xff\xfe)を混ぜたJSONを試します。
invalidUTF8JSON := []byte("{\"Name\":\"\xff\xfe\",\"Age\":30}")
var p3 Person
err3 := jsonv1.Unmarshal(invalidUTF8JSON, &p3)
fmt.Printf("v1: result=%+v err=%v\n", p3, err3)
var p4 Person
err4 := jsonv2.Unmarshal(invalidUTF8JSON, &p4)
fmt.Printf("v2: result=%+v err=%v\n", p4, err4)
出力はこうなりました。
v1: result={Name:�� Age:30} err=<nil>
v2: result={Name: Age:0} err=jsontext: invalid UTF-8 within "/Name" after offset 9
v1は不正なバイト列をそのまま string に詰め込んでエラーなしで返します(表示上は文字化けした � として見えます)。v2は invalid UTF-8 within "/Name" after offset 9 と、どのフィールドの何バイト目で壊れているかまで教えてくれた上でエラーにします。
不正なUTF-8を含む文字列を無検証でDBやログに書き込むと、後段のツールでの文字化けや、文字列長・正規表現マッチの想定外の挙動につながります。v2はこの入口を最初から塞ぎます。
3. Unmarshalは約19%速くなっている
挙動が厳しくなった分、遅くなっていないかも気になったのでベンチマークを取りました。10個のフィールドを持つ構造体に73バイトのJSONをUnmarshalする処理を比較しています。
type Wide struct {
A, B, C, D, E string
F, G, H, I, J int
}
var sample = []byte(`{"A":"a","B":"b","C":"c","D":"d","E":"e","F":1,"G":2,"H":3,"I":4,"J":5}`)
func BenchmarkUnmarshalV1(b *testing.B) {
for i := 0; i < b.N; i++ {
var w Wide
_ = jsonv1.Unmarshal(sample, &w)
}
}
func BenchmarkUnmarshalV2(b *testing.B) {
for i := 0; i < b.N; i++ {
var w Wide
_ = jsonv2.Unmarshal(sample, &w)
}
}
go test -bench=. -benchmem -run=^$ の実行結果です。
BenchmarkUnmarshalV1-4 610438 1897 ns/op 128 B/op 1 allocs/op
BenchmarkUnmarshalV2-4 791710 1540 ns/op 128 B/op 1 allocs/op
v2は1回あたり1,540ns、v1は1,897nsで、v2の方が約19%速い結果になりました。アロケーション数・サイズは同じ(1回・128B)なので、GCへの負荷は変わらず、純粋に処理時間だけが縮んでいます。厳格化と高速化がトレードオフになっていない点は、移行を検討する上で気になっていたところでした。Go 1.27のリリースノートでも「Unmarshalは大幅に高速化された」とアナウンスされており、今回の計測はその主張と一致する結果になっています。
移行する際に確認したいこと
実際に触ってみて、既存コードをv1からv2に切り替える前に確認しておくとよいと感じた点です。
- 外部入力を扱うエンドポイントほど影響が大きい: 自社で生成したJSONだけを扱う内部処理ならほぼ無風ですが、外部クライアントや他社APIのレスポンスを受けるコードは、今までエラーなく通っていたペイロードがv2移行後にエラーで弾かれるようになります。移行前に本番ログのサンプルで一度Unmarshalし直し、新規にエラーになるケースがないか確認するのが安全です。
-
GOEXPERIMENT=nojsonv2で切り戻せる: v1実装自体はv2をベースに作り直されていますが、リリースノートによるとGOEXPERIMENT=nojsonv2で元のv1実装に戻せるとされています。段階移行の逃げ道として覚えておくと安心です。 -
エラーメッセージがそのままデバッグ情報になる: v2のエラーは
jsontext: invalid UTF-8 within "/Name" after offset 9のようにJSON Pointer形式でフィールドとオフセットを示します。v1の「なんとなく変な値が入っている」状態から、ログを見ただけで原因箇所が特定できる状態に変わったのは実務上のメリットです。
まとめ
-
encoding/json/v2は重複キーと不正なUTF-8をデフォルトでエラーにする。v1は両方とも黙って通していた - Unmarshalは同じ入力で約19%速い(1,897ns→1,540ns、アロケーションは同数)
- 外部入力を扱うコードほど、移行前に既存ペイロードでの動作確認をしておく価値がある
関連記事
- plugin validateを通ったSKILL.md 30個が公式仕様の検証で全滅した
- Node.js 26 と 22 を同一コードで実測比較、削除APIで壊れる3点
- OpenCodeのplanモード、read-onlyは権限ルールだけで作られていた
参考リンク
- Go 1.27 Release Notes(encoding/json/v2の仕様・GOEXPERIMENT=nojsonv2)
- Go Release History(Go 1.27.0のリリース日)
-
Go 1.27 Release Notes —
encoding/json/v2とencoding/json/jsontextが新パッケージとして追加された(2026-08-19)。 ↩