[インデックス 18157] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json
パッケージにおける、JSONのアンマーシャリングに関するバグ修正とテストの追加を行っています。具体的には、src/pkg/encoding/json/decode.go
と src/pkg/encoding/json/decode_test.go
の2つのファイルが変更されています。
decode.go
は、JSONデータをGoの構造体にデコードする際の主要なロジックを含んでいます。このファイルでは、decodeState
構造体の object
メソッド内で、json:",string"
オプションが指定されたフィールドの処理に関する修正が行われています。
decode_test.go
は、encoding/json
パッケージのデコード機能に対する単体テストを含んでいます。このコミットでは、修正されたバグを再現し、修正が正しく機能することを確認するための新しいテストケース TestNullString
が追加されています。また、既存のテスト TestUnmarshalNulls
のJSONデータフォーマットも微調整されています。
コミット
このコミットは、encoding/json
パッケージにおいて、json:",string"
オプションを使用して文字列としてエンコードされた数値をGoの整数型にアンマーシャルする際に、null
値が与えられた場合にエラーが適切に報告されないというバグを修正します。特に、連続するアンマーシャリング操作でこの問題が発生する可能性がありました。この修正は、Issue 7046 に対応するものです。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/880442f110ce33b2981561461841979d58848b78
元コミット内容
encoding/json: Fix missing error when trying to unmarshal null string into int, for successive ,string option
Fixes #7046.
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/47260043
変更の背景
この変更は、Goの encoding/json
パッケージが抱えていた特定のバグを修正するために行われました。具体的には、Goの構造体フィールドに json:",string"
タグが付けられている場合、JSONの数値が文字列として表現されていても、Goの整数型に正しくアンマーシャルされることが期待されます。しかし、このフィールドにJSONの null
値が与えられた場合、本来であればエラーとして扱われるべきですが、特定の条件下(特に連続して json:",string"
オプションを持つフィールドをアンマーシャルする際)でエラーが適切に報告されないという問題がありました。
この問題は、GoのIssueトラッカーで Issue 7046 として報告されました。このバグは、開発者がJSONデータをGoの構造体にデコードする際に、予期せぬデータ(null
)が数値フィールドに割り当てられてしまう可能性があり、アプリケーションのロジックに誤動作を引き起こす原因となるため、修正が必要とされました。
前提知識の解説
JSON (JavaScript Object Notation)
JSONは、人間が読み書きしやすく、機械が解析しやすいデータ交換フォーマットです。キーと値のペアの集まり(オブジェクト)と、値の順序付きリスト(配列)という2つの基本的な構造に基づいています。
Go言語の encoding/json
パッケージ
Go言語の標準ライブラリ encoding/json
は、Goのデータ構造とJSONデータの間で変換を行うための機能を提供します。
- マーシャリング (Marshaling): Goのデータ構造をJSONデータに変換すること。
json.Marshal
関数を使用します。 - アンマーシャリング (Unmarshaling): JSONデータをGoのデータ構造に変換すること。
json.Unmarshal
関数を使用します。
構造体タグ json:",string"
Goの構造体フィールドには「構造体タグ」と呼ばれるメタデータを付与できます。encoding/json
パッケージは、このタグを利用してJSONとGoのデータ構造のマッピングを制御します。
json:",string"
タグは、JSONの数値が文字列としてエンコードされている場合に、それをGoの数値型(int
, float64
など)にアンマーシャルするために使用されます。例えば、JSONで {"age": "30"}
のように数値が文字列として表現されている場合でも、Goの構造体フィールドが Age int
json:"age,string" のように定義されていれば、
Ageフィールドに整数値
30` が正しくデコードされます。
null
値の扱い
JSONの null
は、値が存在しないことを示します。Goの encoding/json
パッケージは、通常、JSONの null
をGoのポインタ型やスライス、マップ、インターフェース型にアンマーシャルする際に、それらをゼロ値(nil
)に設定します。しかし、非ポインタのプリミティブ型(int
, string
, bool
など)に null
をアンマーシャルしようとすると、通常はエラーとなります。
今回のバグは、json:",string"
オプションが指定された数値フィールドに null
が与えられた場合に、このエラーが適切に伝播しないというものでした。
技術的詳細
このバグは、encoding/json
パッケージの内部で、json:",string"
オプションが指定されたフィールドのアンマーシャリング処理において、一時的なスクラッチスペース(d.tempstr
)の管理が不適切だったことに起因します。
decodeState
構造体は、JSONデコードの状態を管理します。object
メソッドは、JSONオブジェクトをGoの構造体にデコードする際に呼び出されます。このメソッド内で、各フィールドのアンマーシャリングが行われます。
json:",string"
オプションが指定されている場合(destring
が true
の場合)、encoding/json
はまずJSONの値を文字列として読み込み、その文字列をGoのターゲット型(この場合は整数型)に変換しようとします。この文字列を一時的に保持するために d.tempstr
が使用されます。
バグのシナリオは以下の通りです。
{"A": "1", "B": null}
のようなJSONデータがあり、A
とB
の両方がint
型でjson:",string"
タグを持つ構造体にアンマーシャルされるとします。- まず
A
のアンマーシャリングが行われます。"1"
はd.tempstr
に読み込まれ、その後int
に変換されてA
に格納されます。 - 次に
B
のアンマーシャリングが行われます。null
が読み込まれます。 - 問題は、
null
が読み込まれた後、d.tempstr
がクリアされないままであったことです。もしd.tempstr
に以前のアンマーシャリング(A
のアンマーシャリング)で残った値(例:"1"
)が残っていた場合、null
をint
に変換しようとする際に、この古い値が誤って使用されてしまい、エラーが適切に報告されない可能性がありました。
修正は、d.tempstr
を使用した後、次の値の処理に移る前に d.tempstr
を空文字列 (""
) にリセットすることで、この問題を解決しています。これにより、各アンマーシャリング操作が独立し、以前の操作の残骸が次の操作に影響を与えることがなくなります。
コアとなるコードの変更箇所
src/pkg/encoding/json/decode.go
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -561,6 +561,7 @@ func (d *decodeState) object(v reflect.Value) {
if destring {
d.value(reflect.ValueOf(&d.tempstr))
d.literalStore([]byte(d.tempstr), subv, true)
+ d.tempstr = "" // Zero scratch space for successive values.
} else {
d.value(subv)
}
src/pkg/encoding/json/decode_test.go
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -1060,6 +1060,21 @@ func TestEmptyString(t *testing.T) {
}
}
+// Test that the returned error is non-nil when trying to unmarshal null string into int, for successive ,string option
+// Issue 7046
+func TestNullString(t *testing.T) {
+ type T struct {
+ A int `json:",string"`
+ B int `json:",string"`
+ }
+ data := []byte(`{"A": "1", "B": null}`)
+ var s T
+ err := Unmarshal(data, &s)
+ if err == nil {
+ t.Fatalf("expected error; got %v", s)
+ }
+}
+
func intp(x int) *int {
p := new(int)
*p = x
@@ -1110,8 +1125,8 @@ func TestInterfaceSet(t *testing.T) {
// Issue 2540
func TestUnmarshalNulls(t *testing.T) {
jsonData := []byte(`{
- "Bool" : null,
- "Int" : null,
+ "Bool" : null,
+ "Int" : null,
"Int8" : null,
"Int16" : null,
"Int32" : null,
コアとなるコードの解説
src/pkg/encoding/json/decode.go
の変更
d.tempstr = "" // Zero scratch space for successive values.
この1行が追加されたことで、json:",string"
オプションが指定されたフィールドのアンマーシャリング処理において、d.tempstr
(一時的な文字列バッファ) が次の値の処理に移る前に明示的に空文字列にリセットされるようになりました。
d.value(reflect.ValueOf(&d.tempstr))
: JSONの値を文字列としてd.tempstr
に読み込みます。d.literalStore([]byte(d.tempstr), subv, true)
:d.tempstr
に読み込まれた文字列を、ターゲットのGoの型(この場合はint
)に変換してsubv
(ターゲットフィールドのreflect.Value
) に格納します。d.tempstr = ""
: ここでd.tempstr
をクリアすることで、次のフィールドのアンマーシャリング時に、前のフィールドの処理で残ったデータが誤って使用されることを防ぎます。これにより、null
のような不正な値が与えられた場合に、正しくエラーが検出されるようになります。
src/pkg/encoding/json/decode_test.go
の変更
TestNullString
関数の追加
この新しいテスト関数は、修正されたバグを具体的に再現し、修正が正しく機能することを確認します。
func TestNullString(t *testing.T) {
type T struct {
A int `json:",string"`
B int `json:",string"`
}
data := []byte(`{"A": "1", "B": null}`)
var s T
err := Unmarshal(data, &s)
if err == nil {
t.Fatalf("expected error; got %v", s)
}
}
type T struct { A int
json:",string"; B int
json:",string"}
:json:",string"
タグを持つ2つの整数フィールドA
とB
を持つ構造体T
を定義します。data := []byte(
{"A": "1", "B": null})
: テスト用のJSONデータです。A
は有効な文字列化された数値"1"
、B
はnull
です。err := Unmarshal(data, &s)
: このJSONデータを構造体s
にアンマーシャルします。if err == nil { t.Fatalf("expected error; got %v", s) }
: このテストの重要な部分です。B
フィールドにnull
が与えられているため、アンマーシャリングはエラーを返すことが期待されます。もしエラーが返されなかった場合(err == nil
)、テストは失敗し、バグがまだ存在することを示します。
TestUnmarshalNulls
の変更
既存の TestUnmarshalNulls
関数内のJSONデータフォーマットが、視認性を向上させるために微調整されています。これは機能的な変更ではなく、コードの整形に関するものです。
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -1110,8 +1125,8 @@ func TestInterfaceSet(t *testing.T) {
// Issue 2540
func TestUnmarshalNulls(t *testing.T) {
jsonData := []byte(`{
- "Bool" : null,
- "Int" : null,
+ "Bool" : null,
+ "Int" : null,
"Int8" : null,
"Int16" : null,
"Int32" : null,
:
の後のスペースが削除され、より一貫したフォーマットになっています。
関連リンク
- Go Issue 7046: encoding/json: missing error when unmarshaling null string into int, for successive ,string option
- Go CL 47260043: encoding/json: Fix missing error when trying to unmarshal null string into int, for successive ,string option