Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 18157] ファイルの概要

このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおける、JSONのアンマーシャリングに関するバグ修正とテストの追加を行っています。具体的には、src/pkg/encoding/json/decode.gosrc/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" オプションが指定されている場合(destringtrue の場合)、encoding/json はまずJSONの値を文字列として読み込み、その文字列をGoのターゲット型(この場合は整数型)に変換しようとします。この文字列を一時的に保持するために d.tempstr が使用されます。

バグのシナリオは以下の通りです。

  1. {"A": "1", "B": null} のようなJSONデータがあり、AB の両方が int 型で json:",string" タグを持つ構造体にアンマーシャルされるとします。
  2. まず A のアンマーシャリングが行われます。"1"d.tempstr に読み込まれ、その後 int に変換されて A に格納されます。
  3. 次に B のアンマーシャリングが行われます。null が読み込まれます。
  4. 問題は、null が読み込まれた後、d.tempstr がクリアされないままであったことです。もし d.tempstr に以前のアンマーシャリング(A のアンマーシャリング)で残った値(例: "1")が残っていた場合、nullint に変換しようとする際に、この古い値が誤って使用されてしまい、エラーが適切に報告されない可能性がありました。

修正は、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つの整数フィールド AB を持つ構造体 T を定義します。
  • data := []byte({"A": "1", "B": null}): テスト用のJSONデータです。A は有効な文字列化された数値 "1"Bnull です。
  • 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,

: の後のスペースが削除され、より一貫したフォーマットになっています。

関連リンク

参考にした情報源リンク