0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Go 1.27のjson/v2で重複キーを黙って上書きしなくなった

0
Posted at

はじめに

Go 1.27が2026年8月19日に正式リリースされました。目玉のひとつが標準ライブラリに新しく加わった encoding/json/v2 です1。既存の encoding/json(以下v1)と同じような使い方ができますが、パッケージ名を変えただけの別実装ではなく、デコード時の既定挙動そのものが変わっています。対象読者は、Go でJSONを扱うAPIサーバーやCLIツールを書いているエンジニアです。

本記事では、Go 1.27を実際にインストールして encoding/jsonencoding/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、アロケーションは同数)
  • 外部入力を扱うコードほど、移行前に既存ペイロードでの動作確認をしておく価値がある

関連記事

参考リンク

  1. Go 1.27 Release Notesencoding/json/v2encoding/json/jsontext が新パッケージとして追加された(2026-08-19)。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?