[インデックス 14882] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおける、型ミスマッチによるパニック(panic)を修正するものです。具体的には、JSONデコード時にGoのインターフェース型への値の割り当てが不適切に行われる場合に発生していた問題に対処しています。特に、reflect.Interface 型の変数がメソッドを持たない(つまり、nil インターフェースまたは空のインターフェース)場合にのみ、非リフレクションベースの高速なデコードパスを使用するように変更し、それ以外の場合は型ミスマッチとしてエラーを報告するように改善されています。これにより、予期せぬパニックを防ぎ、より堅牢なJSONデコード処理を実現しています。
コミット
- コミットハッシュ:
406ca3c2f19a0742a05e5837ca9cae77fb4cadd8 - 作者: Rémy Oudompheng oudomphe@phare.normalesup.org
- コミット日時: Mon Jan 14 08:44:16 2013 +0100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/406ca3c2f19a0742a05e5837ca9cae77fb4cadd8
元コミット内容
encoding/json: fix panics on type mismatches.
Fixes #4222.
Fixes #4628.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/7100049
変更の背景
このコミットは、Goの encoding/json パッケージが特定の状況下で型ミスマッチに遭遇した際にパニックを引き起こす問題を解決するために導入されました。具体的には、以下の2つのIssueを修正しています。
-
Issue 4222:
json.Unmarshalpanics when unmarshaling intoerrortype: この問題は、json.Unmarshalを使用してJSONデータをGoのerror型の変数にデコードしようとした際にパニックが発生するというものでした。error型はGoのインターフェース型であり、encoding/jsonパッケージがreflectパッケージを使って型情報を処理する際に、特定のインターフェース型(特にメソッドを持つインターフェース)への不適切なデコード試行がパニックにつながっていました。例えば、JSONのオブジェクト{}や配列[]をerror型にデコードしようとするとパニックが発生していました。 -
Issue 4628:
json.Unmarshalpanics when unmarshaling into*stringwith{"user": "name"}: この問題は、JSONオブジェクトを*string型(文字列へのポインタ)にデコードしようとした際にパニックが発生するというものでした。encoding/jsonは、JSONオブジェクトを文字列にデコードしようとすると型ミスマッチとしてUnmarshalTypeErrorを返すのが正しい挙動ですが、このケースではパニックが発生していました。これは、reflect.Interface型の処理ロジックが、特定の条件下で誤ったデコードパスを選択していたことに起因します。
これらのパニックは、encoding/json パッケージの堅牢性を損なうものであり、開発者が予期せぬクラッシュに遭遇する原因となっていました。このコミットは、reflect パッケージを用いた型チェックとデコードロジックを改善することで、これらのパニックを修正し、より適切なエラーハンドリング(UnmarshalTypeError の返却)を行うことを目的としています。
前提知識の解説
このコミットの理解には、以下のGo言語の概念と encoding/json パッケージの内部動作に関する知識が役立ちます。
-
Go言語の
interface{}(空インターフェース):interface{}は、Go言語における「任意の型」を表す特別なインターフェースです。これはメソッドを一切持たないため、どのような型の値でもinterface{}型の変数に代入できます。encoding/jsonパッケージでは、デコード先の型がinterface{}の場合、JSONのデータ型(文字列、数値、ブール値、配列、オブジェクト)に応じて適切なGoの型(string,float64,bool,[]interface{},map[string]interface{})に動的に変換して格納します。 -
Go言語の
reflectパッケージ:reflectパッケージは、Goプログラムが実行時に自身の構造(型、値、メソッドなど)を検査・操作するための機能を提供します。encoding/jsonのような汎用的なデータシリアライズ/デシリアライズライブラリは、このreflectパッケージを多用して、デコード先のGoの型情報を取得し、JSONデータをその型に合わせて適切に変換・格納します。reflect.Value: Goの変数の実行時の値を表します。reflect.Kind():reflect.Valueが表す値の基本的な種類(reflect.Int,reflect.String,reflect.Slice,reflect.Interfaceなど)を返します。reflect.NumMethod():reflect.Valueが表す型が持つメソッドの数を返します。インターフェース型の場合、これはそのインターフェースが定義するメソッドの数を意味します。interface{}(空インターフェース) の場合、NumMethod()は0を返します。
-
encoding/jsonパッケージのデコード処理:encoding/jsonパッケージのUnmarshal関数は、JSONバイト列をGoのデータ構造にデコードします。この際、デコード先のGoの型がinterface{}の場合、encoding/jsonは内部的にarrayInterface()やobjectInterface()といった非リフレクションベースの最適化されたデコードパスを使用することがあります。これは、interface{}へのデコードは型が事前に決まっていないため、リフレクションのオーバーヘッドを避けて直接Goのプリミティブ型やmap[string]interface{}、[]interface{}に変換できるためです。 -
UnmarshalTypeError:encoding/jsonパッケージが、JSONのデータ型とGoのデコード先の型が一致しない場合に返すエラー型です。例えば、JSONの文字列"hello"をGoのint型にデコードしようとした場合などに発生します。このエラーは、パニックではなく、プログラムで捕捉して適切に処理できるエラーとして設計されています。
このコミットの核心は、reflect.Interface 型の変数をデコードする際に、そのインターフェースが「メソッドを持たない空インターフェース (interface{})」なのか、それとも「特定のメソッドを持つインターフェース(例: error 型)」なのかを v.NumMethod() == 0 で厳密に区別し、それに応じてデコードロジックを分岐させる点にあります。これにより、空インターフェースの場合のみ最適化されたパスを使い、それ以外のインターフェース型への不適切なデコード試行は UnmarshalTypeError として適切にエラー報告されるようになります。
技術的詳細
このコミットの技術的変更は、主に src/pkg/encoding/json/decode.go ファイル内の decodeState 構造体のメソッド、特に array, object, literalStore に集中しています。これらのメソッドは、JSONの配列、オブジェクト、およびリテラル値(真偽値、文字列、数値)をGoの対応する型にデコードする役割を担っています。
変更の核心は、デコード先のGoの型が reflect.Interface である場合の処理ロジックの修正です。
-
decodeState.array(v reflect.Value)メソッドの変更: JSON配列をGoの型にデコードする際の処理です。 変更前は、reflect.Interface型へのデコードの場合、無条件にd.arrayInterface()を呼び出して非リフレクションベースのデコードを行っていました。 変更後は、reflect.Interface型の場合に加えて、v.NumMethod() == 0という条件が追加されました。これは、デコード先のインターフェースがメソッドを持たない(つまりinterface{}型である)場合にのみ、d.arrayInterface()を呼び出すようにします。 もしインターフェースがメソッドを持つ(例:error型)場合、fallthroughを使ってdefaultケースに処理を移し、UnmarshalTypeErrorを発生させるように変更されました。これにより、JSON配列をerror型のようなメソッドを持つインターフェースにデコードしようとした際に、パニックではなく適切なエラーが返されるようになります。 -
decodeState.object(v reflect.Value)メソッドの変更: JSONオブジェクトをGoの型にデコードする際の処理です。 変更前は、v.Kind() == reflect.Interfaceであれば無条件にd.objectInterface()を呼び出していました。 変更後は、v.Kind() == reflect.Interface && v.NumMethod() == 0という条件に厳格化されました。これにより、JSONオブジェクトをデコードする際に、デコード先のインターフェースがinterface{}型である場合にのみ、最適化されたd.objectInterface()パスが使用されます。それ以外のメソッドを持つインターフェース型へのデコード試行は、defaultケースでUnmarshalTypeErrorを発生させるように変更されました。 -
decodeState.literalStore(item []byte, v reflect.Value, fromQuoted bool)メソッドの変更: JSONのリテラル値(真偽値、文字列、数値)をGoの型にデコードする際の処理です。 このメソッドは、JSONの真偽値、文字列、数値のそれぞれについて、デコード先のGoの型がreflect.Interfaceである場合の処理を修正しています。- 真偽値 (
case 't',case 'f'): 変更前は、reflect.Interface型であれば無条件にv.Set(reflect.ValueOf(value))で値をセットしていました。 変更後は、v.NumMethod() == 0の条件が追加され、空インターフェースの場合のみ値をセットします。それ以外の場合はUnmarshalTypeErrorを発生させます。 - 文字列 (
case '"'): 変更前は、reflect.Interface型であれば無条件にv.Set(reflect.ValueOf(string(s)))で値をセットしていました。 変更後は、v.NumMethod() == 0の条件が追加され、空インターフェースの場合のみ値をセットします。それ以外の場合はUnmarshalTypeErrorを発生させます。 - 数値 (
default): 変更前は、reflect.Interface型であれば無条件にv.Set(reflect.ValueOf(n))で値をセットしていました。 変更後は、v.NumMethod() != 0の条件が追加され、メソッドを持つインターフェース型の場合にUnmarshalTypeErrorを発生させます。
- 真偽値 (
これらの変更により、encoding/json は、デコード先のインターフェースが interface{} (メソッドを持たない空インターフェース) である場合にのみ、そのJSONの型に応じたGoの具体的な型(string, float64, bool, []interface{}, map[string]interface{})にデコードします。それ以外の、特定のメソッドを持つインターフェース型(例: error 型)に対して、JSONのデータ型がそのインターフェースの期待する型と一致しない場合(例: JSONオブジェクトを error 型にデコードしようとする場合)は、パニックではなく UnmarshalTypeError を返すように修正されました。
テストファイルの変更 (src/pkg/encoding/json/decode_test.go):
新しいテストケース decodeTypeErrorTests が追加され、TestUnmarshalTypeError 関数でこれらの修正が正しく機能するか検証しています。
{new(string),{"user": "name"}}: Issue 4628 のケースをカバーし、JSONオブジェクトを*stringにデコードしようとした際にUnmarshalTypeErrorが発生することを確認します。{new(error),{}},{new(error),[]},{new(error),""},{new(error),123},{new(error),true}: Issue 4222 のケースをカバーし、様々なJSONデータ型をerror型にデコードしようとした際にUnmarshalTypeErrorが発生することを確認します。
これらのテストは、以前パニックを引き起こしていたシナリオが、適切に UnmarshalTypeError を返すようになったことを保証します。
コアとなるコードの変更箇所
diff --git a/src/pkg/encoding/json/decode.go b/src/pkg/encoding/json/decode.go
index d86fd7711b..eb8c75b24a 100644
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -347,15 +347,19 @@ func (d *decodeState) array(v reflect.Value) {
// Check type of target.
switch v.Kind() {
+\tcase reflect.Interface:
+\t\tif v.NumMethod() == 0 {
+\t\t\t// Decoding into nil interface? Switch to non-reflect code.
+\t\t\tv.Set(reflect.ValueOf(d.arrayInterface()))
+\t\t\treturn
+\t\t}
+\t\t// Otherwise it\'s invalid.
+\t\tfallthrough
\tdefault:
\t\td.saveError(&UnmarshalTypeError{\"array\", v.Type()})\
\t\td.off--
\t\td.next()\
\t\treturn
-\tcase reflect.Interface:\n-\t\t// Decoding into nil interface? Switch to non-reflect code.\n-\t\tv.Set(reflect.ValueOf(d.arrayInterface()))\n-\t\treturn
\tcase reflect.Array:\n \tcase reflect.Slice:\n \t\tbreak
@@ -441,7 +445,7 @@ func (d *decodeState) object(v reflect.Value) {
\tv = pv
// Decoding into nil interface? Switch to non-reflect code.
-\tif v.Kind() == reflect.Interface {\n+\tif v.Kind() == reflect.Interface && v.NumMethod() == 0 {\n \t\tv.Set(reflect.ValueOf(d.objectInterface()))\n \t\treturn
\t}
@@ -459,11 +463,9 @@ func (d *decodeState) object(v reflect.Value) {
\t\t\tv.Set(reflect.MakeMap(t))\n \t\t}\n \tcase reflect.Struct:\n+\n \tdefault:\n \t\td.saveError(&UnmarshalTypeError{\"object\", v.Type()})\n-\t}\n-\n-\tif !v.IsValid() {\n \t\td.off--\n \t\td.next() // skip over { } in input\n \t\treturn
@@ -646,7 +648,11 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool\n \t\tcase reflect.Bool:\n \t\t\tv.SetBool(value)\n \t\tcase reflect.Interface:\n-\t\t\tv.Set(reflect.ValueOf(value))\n+\t\t\tif v.NumMethod() == 0 {\n+\t\t\t\tv.Set(reflect.ValueOf(value))\n+\t\t\t} else {\n+\t\t\t\td.saveError(&UnmarshalTypeError{\"bool\", v.Type()})\n+\t\t\t}\n \t\t}\n \n \tcase \'\"\': // string\n@@ -676,7 +682,11 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool\n \t\tcase reflect.String:\n \t\t\tv.SetString(string(s))\n \t\tcase reflect.Interface:\n-\t\t\tv.Set(reflect.ValueOf(string(s)))\n+\t\t\tif v.NumMethod() == 0 {\n+\t\t\t\tv.Set(reflect.ValueOf(string(s)))\n+\t\t\t} else {\n+\t\t\t\td.saveError(&UnmarshalTypeError{\"string\", v.Type()})\n+\t\t\t}\n \t\t}\n \n \tdefault: // number\n@@ -705,6 +715,10 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool\n \t\t\t\td.saveError(err)\n \t\t\t\tbreak\n \t\t\t}\n+\t\t\tif v.NumMethod() != 0 {\n+\t\t\t\td.saveError(&UnmarshalTypeError{\"number\", v.Type()})\n+\t\t\t\tbreak\n+\t\t\t}\n \t\t\tv.Set(reflect.ValueOf(n))\n \n \t\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\ndiff --git a/src/pkg/encoding/json/decode_test.go b/src/pkg/encoding/json/decode_test.go
index 562b5b5d88..4c75f19f4a 100644
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -1042,3 +1042,25 @@ func TestStringKind(t *testing.T) {\n \t}\n \n }\n+\n+var decodeTypeErrorTests = []struct {\n+\tdest interface{}\n+\tsrc string\n+}{\n+\t{new(string), `{\"user\": \"name\"}`}, // issue 4628.\n+\t{new(error), `{}`}, // issue 4222\n+\t{new(error), `[]`},\n+\t{new(error), `\"\"`},\n+\t{new(error), `123`},\n+\t{new(error), `true`},\n+}\n+\n+func TestUnmarshalTypeError(t *testing.T) {\n+\tfor _, item := range decodeTypeErrorTests {\n+\t\terr := Unmarshal([]byte(item.src), item.dest)\n+\t\tif _, ok := err.(*UnmarshalTypeError); !ok {\n+\t\t\tt.Errorf(\"expected type error for Unmarshal(%q, type %T): got %v instead\",\n+\t\t\t\titem.src, item.dest, err)\n+\t\t}\n+\t}\n+}\n```
## コアとなるコードの解説
### `src/pkg/encoding/json/decode.go`
1. **`func (d *decodeState) array(v reflect.Value)`**:
* **変更前**: `reflect.Interface` のケースが独立しており、無条件に `d.arrayInterface()` を呼び出していました。これは、メソッドを持つインターフェース(例: `error`)にJSON配列をデコードしようとした場合でも、空インターフェースと同様に処理しようとし、結果としてパニックを引き起こす可能性がありました。
* **変更後**:
```go
case reflect.Interface:
if v.NumMethod() == 0 {
// Decoding into nil interface? Switch to non-reflect code.
v.Set(reflect.ValueOf(d.arrayInterface()))
return
}
// Otherwise it's invalid.
fallthrough
default:
d.saveError(&UnmarshalTypeError{"array", v.Type()})
d.off--
d.next()
return
```
`reflect.Interface` のケースが `default` の前に移動し、`v.NumMethod() == 0` という条件が追加されました。
* `v.NumMethod() == 0` の場合: これはデコード先のインターフェースが `interface{}` (空インターフェース) であることを意味します。この場合のみ、最適化された非リフレクションコードパスである `d.arrayInterface()` が呼び出され、JSON配列は `[]interface{}` にデコードされます。
* `v.NumMethod() != 0` の場合: これはデコード先のインターフェースがメソッドを持つ(例: `error` 型)ことを意味します。この場合、`fallthrough` を使って `default` ケースに処理を移します。`default` ケースでは、`UnmarshalTypeError` が生成され、JSON配列をメソッドを持つインターフェースにデコードしようとした型ミスマッチとして適切にエラーが報告されます。これにより、パニックが回避されます。
2. **`func (d *decodeState) object(v reflect.Value)`**:
* **変更前**: `if v.Kind() == reflect.Interface { ... }` という条件で、無条件に `d.objectInterface()` を呼び出していました。これも `array` メソッドと同様に、メソッドを持つインターフェースへの不適切なデコード試行でパニックの原因となっていました。
* **変更後**: `if v.Kind() == reflect.Interface && v.NumMethod() == 0 { ... }` に条件が変更されました。
これにより、デコード先のインターフェースが `interface{}` (空インターフェース) である場合にのみ、最適化された `d.objectInterface()` パスが使用され、JSONオブジェクトは `map[string]interface{}` にデコードされます。メソッドを持つインターフェース型へのデコード試行は、この `if` 文の条件を満たさないため、後続の `default` ケース(変更後のコードでは省略されていますが、`UnmarshalTypeError` を返すロジックが適用される)で適切にエラー処理されます。
3. **`func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool)`**:
この関数は、JSONの真偽値、文字列、数値といったリテラル値をGoの型にデコードします。それぞれのケースで `reflect.Interface` 型へのデコードロジックが修正されています。
* **真偽値 (`case reflect.Bool`)**:
```go
case reflect.Interface:
if v.NumMethod() == 0 {
v.Set(reflect.ValueOf(value))
} else {
d.saveError(&UnmarshalTypeError{"bool", v.Type()})
}
```
`v.NumMethod() == 0` の条件が追加され、空インターフェースの場合のみ真偽値をセットします。それ以外(メソッドを持つインターフェース)の場合は `UnmarshalTypeError` を発生させます。
* **文字列 (`case reflect.String`)**:
```go
case reflect.Interface:
if v.NumMethod() == 0 {
v.Set(reflect.ValueOf(string(s)))
} else {
d.saveError(&UnmarshalTypeError{"string", v.Type()})
}
```
同様に `v.NumMethod() == 0` の条件が追加され、空インターフェースの場合のみ文字列をセットします。それ以外の場合は `UnmarshalTypeError` を発生させます。
* **数値 (`default` - 数値の処理部分)**:
```go
if v.NumMethod() != 0 {
d.saveError(&UnmarshalTypeError{"number", v.Type()})
break
}
v.Set(reflect.ValueOf(n))
```
数値のデコード処理において、`v.NumMethod() != 0` の条件が追加されました。これにより、メソッドを持つインターフェース型に数値をデコードしようとした場合、`UnmarshalTypeError` が発生し、パニックが回避されます。
### `src/pkg/encoding/json/decode_test.go`
* **`decodeTypeErrorTests` 変数**:
様々な型ミスマッチのシナリオを網羅するテストデータが追加されました。特に、`new(string)` にJSONオブジェクトをデコードするケース(Issue 4628)と、`new(error)` に様々なJSONデータ型をデコードするケース(Issue 4222)が含まれています。
* **`TestUnmarshalTypeError` 関数**:
`decodeTypeErrorTests` の各項目について `json.Unmarshal` を実行し、返されたエラーが期待通り `*UnmarshalTypeError` であることを検証しています。これにより、以前パニックを引き起こしていた状況が、適切にエラーとして処理されるようになったことが確認されます。
これらの変更により、`encoding/json` パッケージは、デコード先のGoの型とJSONデータの型がミスマッチした場合に、より予測可能で堅牢なエラーハンドリングを提供するようになりました。
## 関連リンク
* GitHubコミットページ: [https://github.com/golang/go/commit/406ca3c2f19a0742a05e5837ca9cae77fb4cadd8](https://github.com/golang/go/commit/406ca3c2f19a0742a05e5837ca9cae77fb4cadd8)
* Go Issue 4222: `json.Unmarshal` panics when unmarshaling into `error` type (現在は閉鎖)
* Go Issue 4628: `json.Unmarshal` panics when unmarshaling into `*string` with `{"user": "name"}` (現在は閉鎖)
* Go CL 7100049: [https://golang.org/cl/7100049](https://golang.org/cl/7100049) (Go Code Review)
## 参考にした情報源リンク
* Go言語公式ドキュメント: `encoding/json` パッケージ: [https://pkg.go.dev/encoding/json](https://pkg.go.dev/encoding/json)
* Go言語公式ドキュメント: `reflect` パッケージ: [https://pkg.go.dev/reflect](https://pkg.go.dev/reflect)
* Go言語のインターフェースについて: [https://go.dev/tour/methods/10](https://go.dev/tour/methods/10)
* Go言語の `interface{}` について: [https://go.dev/blog/effective-go#interfaces](https://go.dev/blog/effective-go#interfaces)
* Go言語の `error` インターフェースについて: [https://go.dev/blog/error-handling-and-go](https://go.dev/blog/error-handling-and-go)
* Go言語の `reflect.Value.NumMethod()` の挙動について (一般的なリフレクションの解説): [https://pkg.go.dev/reflect#Value.NumMethod](https://pkg.go.dev/reflect#Value.NumMethod)