[インデックス 14962] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json
パッケージにおけるJSONデコード時の挙動変更に関するものです。具体的には、JSONのキーがGoの構造体のエクスポートされていない(unexported)フィールド名と一致した場合の処理が修正されています。Go 1.0ではこの場合に UnmarshalFieldError
が発生していましたが、この変更によりエラーを発生させずに該当フィールドをスキップするようになります。
コミット
commit 6e3f3af4e0f4293f868aa71c4271cab6344d5797
Author: Rick Arnold <rickarnoldjr@gmail.com>
Date: Tue Jan 22 17:49:07 2013 -0500
encoding/json: ignore unexported fields in Unmarshal
Go 1.0 behavior was to create an UnmarshalFieldError when a json value name matched an unexported field name. This error will no longer be created and the field will be skipped instead.
Fixes #4660.
R=adg, rsc, bradfitz
CC=golang-dev
https://golang.org/cl/7139049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6e3f3af4e0f4293f868aa71c4271cab6344d5797
元コミット内容
Go 1.0では、encoding/json
パッケージの Unmarshal
関数がJSON値をGoの構造体にデコードする際、JSONのキーが構造体のエクスポートされていないフィールド(小文字で始まるフィールド)と一致した場合に UnmarshalFieldError
を発生させていました。このコミットは、この挙動を変更し、エラーを発生させる代わりに、該当するエクスポートされていないフィールドを単にスキップするようにします。これにより、JSONデコード時のエラーハンドリングがより柔軟になります。
この変更は、GoのIssue #4660を修正するものです。ただし、現在のGoのIssueトラッカーで #4660 を検索すると別のIssueが表示されるため、このコミットが参照しているのは古い、既にクローズされたIssueである可能性が高いです。
変更の背景
Go言語の設計思想の一つに「明示的なエラーハンドリング」がありますが、同時に「不要なエラーは発生させない」という考え方もあります。encoding/json
パッケージは、Goの構造体のエクスポートされたフィールド(大文字で始まるフィールド)のみをJSONとの間でマッピングするという明確なルールを持っています。エクスポートされていないフィールドは、パッケージ外部からはアクセスできないため、JSONデコードの対象外とすることが自然な挙動です。
Go 1.0の時点では、JSONキーとエクスポートされていないフィールド名が一致した場合にエラーを発生させることで、開発者にその事実を通知していました。しかし、これは場合によっては過剰なエラーであり、開発者が意図的にエクスポートされていないフィールドをJSONデコードの対象外としたい場合に、不必要なエラーハンドリングを強制することになります。
この変更の背景には、encoding/json
の Unmarshal
の挙動をよりGoの慣習に合わせ、エクスポートされていないフィールドは「無視されるべきもの」として扱うという方針転換があったと考えられます。これにより、開発者はJSONデータにエクスポートされていないフィールド名と一致するキーが含まれていても、それが意図しないエラーとして扱われることを心配する必要がなくなります。
前提知識の解説
Go言語におけるエクスポートされたフィールドとエクスポートされていないフィールド
Go言語では、識別子(変数名、関数名、型名、構造体のフィールド名など)の最初の文字が大文字か小文字かによって、その識別子が「エクスポートされている(exported)」か「エクスポートされていない(unexported)」かが決まります。
- エクスポートされた識別子(Exported Identifiers): 最初の文字が大文字の識別子です。これらは、その識別子が定義されているパッケージの外部からアクセス可能です。例えば、
json.Marshal
やfmt.Println
のMarshal
やPrintln
はエクスポートされた関数です。構造体のフィールドの場合、エクスポートされたフィールドは他のパッケージから直接アクセスしたり、JSONエンコード/デコードの対象になったりします。 - エクスポートされていない識別子(Unexported Identifiers): 最初の文字が小文字の識別子です。これらは、その識別子が定義されているパッケージ内からのみアクセス可能です。パッケージの外部からは直接アクセスできません。構造体のフィールドの場合、エクスポートされていないフィールドは、その構造体が定義されているパッケージ内でのみ利用され、通常、
encoding/json
のような外部パッケージからは無視されます。
encoding/json
パッケージの Unmarshal
encoding/json
パッケージは、Goのデータ構造とJSONデータの間で変換を行うための標準ライブラリです。
json.Marshal
: Goのデータ構造(通常は構造体)をJSON形式のバイトスライスにエンコード(マーシャル)します。この際、エクスポートされたフィールドのみがJSONに出力されます。json.Unmarshal
: JSON形式のバイトスライスをGoのデータ構造にデコード(アンマーシャル)します。この際、JSONのキーとGoの構造体のエクスポートされたフィールド名が一致した場合に、そのフィールドに値が設定されます。
Goの encoding/json
パッケージは、デフォルトでエクスポートされたフィールドのみを処理するという設計思想に基づいています。これは、Goのアクセス制御メカニズムと一貫しており、外部に公開すべきでない内部的なデータはJSONシリアライズ/デシリアライズの対象外とすることで、カプセル化を促進します。
UnmarshalFieldError
(Go 1.0での挙動)
Go 1.0の encoding/json
パッケージでは、Unmarshal
処理中にJSONのキーが構造体のエクスポートされていないフィールド名と一致した場合、UnmarshalFieldError
というエラーを返していました。このエラーは、開発者に対して「JSONデータに、Goの構造体では処理できない(エクスポートされていない)フィールドが含まれている」ことを明示的に通知する目的がありました。
しかし、この挙動は、JSONデータがGoの構造体と完全に一致しない場合(例えば、JSONデータがより多くのフィールドを持っている場合や、Goの構造体がJSONデータの一部しか必要としない場合)に、不必要なエラーを引き起こす可能性がありました。多くのケースでは、エクスポートされていないフィールドに対応するJSONキーは単に無視されることが期待されます。
技術的詳細
このコミットの技術的な変更点は、encoding/json
パッケージの decode.go
ファイル内の object
メソッドにおける、エクスポートされていないフィールドに対するエラーチェックロジックの削除です。
Goの encoding/json
パッケージは、リフレクション(reflect
パッケージ)を使用して、Goの構造体とJSONデータの間のマッピングを行います。Unmarshal
関数がJSONオブジェクトをGoの構造体にデコードする際、内部的には decodeState
構造体の object
メソッドが呼び出されます。このメソッドは、JSONオブジェクトの各キーと値のペアを反復処理し、対応するGoの構造体フィールドを探します。
変更前のコードでは、JSONキーに対応するエクスポートされたフィールドが見つからなかった場合、さらに構造体のエクスポートされていないフィールドをスキャンし、JSONキーと名前が一致するエクスポートされていないフィールドがあれば UnmarshalFieldError
を生成していました。このスキャンは、f.PkgPath != ""
という条件でエクスポートされていないフィールドを識別し、strings.EqualFold(f.Name, key)
で大文字小文字を区別せずに名前の一致を確認していました。
このコミットでは、このエラーチェックロジック全体が削除されました。これにより、JSONキーがエクスポートされていないフィールド名と一致しても、エラーは発生せず、単にそのキーと値のペアは無視されるようになります。
この変更は、encoding/json
の Unmarshal
の挙動を、エクスポートされていないフィールドは「無視されるべきもの」というGoの慣習に完全に合わせるものです。これにより、JSONデータがGoの構造体よりも多くのフィールドを持っていても、それらがエクスポートされていないフィールドに対応する限り、エラーなしでデコードが成功するようになります。
コアとなるコードの変更箇所
src/pkg/encoding/json/decode.go
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -106,6 +106,7 @@ func (e *UnmarshalTypeError) Error() string {
// An UnmarshalFieldError describes a JSON object key that
// led to an unexported (and therefore unwritable) struct field.
+// (No longer used; kept for compatibility.)
type UnmarshalFieldError struct {
Key string
Type reflect.Type
@@ -530,15 +531,6 @@ func (d *decodeState) object(v reflect.Value) {
subv = subv.Field(i)
}
} else {
- // To give a good error, a quick scan for unexported fields in top level.
- st := v.Type()
- for i := 0; i < st.NumField(); i++ {
- f := st.Field(i)
- if f.PkgPath != "" && strings.EqualFold(f.Name, key) {
- d.saveError(&UnmarshalFieldError{key, st, f})
- }
- }
}
}
src/pkg/encoding/json/decode_test.go
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -199,7 +199,7 @@ var unmarshalTests = []unmarshalTest{
{in: `\"invalid: \\uD834x\\uDD1E\"`, ptr: new(string), out: \"invalid: \\uFFFDx\\uFFFD\"},\n \t{in: \"null\", ptr: new(interface{}), out: nil},\n \t{in: `{\"X\": [1,2,3], \"Y\": 4}`, ptr: new(T), out: T{Y: 4}, err: &UnmarshalTypeError{\"array\", reflect.TypeOf(\"\")}},\n-\t{in: `{\"x\": 1}`, ptr: new(tx), out: tx{}, err: &UnmarshalFieldError{\"x\", txType, txType.Field(0)}},\n+\t{in: `{\"x\": 1}`, ptr: new(tx), out: tx{}},\n \t{in: `{\"F1\":1,\"F2\":2,\"F3\":3}`, ptr: new(V), out: V{F1: float64(1), F2: int32(2), F3: Number(\"3\")}},\n \t{in: `{\"F1\":1,\"F2\":2,\"F3\":3}`, ptr: new(V), out: V{F1: Number(\"1\"), F2: int32(2), F3: Number(\"3\")}, useNumber: true},\n \t{in: `{\"k1\":1,\"k2\":\"s\",\"k3\":[1,2.0,3e-3],\"k4\":{\"kk1\":\"s\",\"kk2\":2}}`, ptr: new(interface{}), out: ifaceNumAsFloat64},\n@@ -1064,3 +1064,25 @@ func TestUnmarshalTypeError(t *testing.T) {\n \t\t}\n \t}\n }\n+\n+// Test handling of unexported fields that should be ignored.\n+// Issue 4660\n+type unexportedFields struct {\n+\tName string\n+\tm map[string]interface{} `json:\"-\"`\n+\tm2 map[string]interface{} `json:\"abcd\"`\n+}\n+\n+func TestUnmarshalUnexported(t *testing.T) {\n+\tinput := `{\"Name\": \"Bob\", \"m\": {\"x\": 123}, \"m2\": {\"y\": 456}, \"abcd\": {\"z\": 789}}`\n+\twant := &unexportedFields{Name: \"Bob\"}\n+\n+\tout := &unexportedFields{}\n+\terr := Unmarshal([]byte(input), out)\n+\tif err != nil {\n+\t\tt.Errorf(\"got error %v, expected nil\", err)\n+\t}\n+\tif !reflect.DeepEqual(out, want) {\n+\t\tt.Errorf(\"got %q, want %q\", out, want)\n+\t}\n+}\n```
## コアとなるコードの解説
### `src/pkg/encoding/json/decode.go` の変更
`UnmarshalFieldError` の定義には、コメント `// (No longer used; kept for compatibility.)` が追加され、このエラー型がもはや内部的に使用されないことが明示されています。これは、既存のコードがこのエラー型を参照している場合に互換性を保つための措置です。
最も重要な変更は、`decodeState` 構造体の `object` メソッド内で行われています。
```go
} else {
// To give a good error, a quick scan for unexported fields in top level.
st := v.Type()
for i := 0; i < st.NumField(); i++ {
f := st.Field(i)
if f.PkgPath != "" && strings.EqualFold(f.Name, key) {
d.saveError(&UnmarshalFieldError{key, st, f})
}
}
}
上記の else
ブロック全体が削除されています。このブロックは、JSONキーに対応するエクスポートされたフィールドが見つからなかった場合に、エクスポートされていないフィールドを探索し、一致するものがあれば UnmarshalFieldError
を生成する役割を担っていました。このコードが削除されたことで、JSONキーがエクスポートされていないフィールド名と一致しても、エラーは発生せず、そのキーと値のペアは単に無視されるようになります。
src/pkg/encoding/json/decode_test.go
の変更
テストファイル decode_test.go
では、この変更を反映するために2つの修正が行われています。
-
unmarshalTests
スライス内の既存のテストケースの修正:{in:
{"x": 1}, ptr: new(tx), out: tx{}, err: &UnmarshalFieldError{\"x\", txType, txType.Field(0)}},
この行は、tx
という構造体のエクスポートされていないフィールドx
にJSON値をデコードしようとした際にUnmarshalFieldError
が発生することを期待するテストケースでした。変更後、このエラーは発生しなくなるため、テストケースは以下のように修正されています。{in:
{"x": 1}, ptr: new(tx), out: tx{}},
err
フィールドが削除され、エラーが発生しないことを期待するようになりました。 -
新しいテストケース
TestUnmarshalUnexported
の追加: この新しいテスト関数は、エクスポートされていないフィールドが正しく無視されることを検証します。unexportedFields
という構造体が定義されており、m
とm2
という2つのエクスポートされていないフィールドが含まれています。m
にはjson:"-"
タグが付いており、JSONエンコード/デコードから完全に除外されることを示します。m2
にはjson:"abcd"
タグが付いていますが、これはエクスポートされていないフィールドであるため、JSONデコード時には無視されるべきです。 テスト入力input
には、"m"
と"abcd"
というキーが含まれています。 期待される出力want
は、Name
フィールドのみがデコードされ、m
とm2
は初期値のままであることを示しています。Unmarshal
を実行した後、エラーが発生しないこと、そしてデコードされた構造体が期待される値と一致すること(つまり、エクスポートされていないフィールドが無視されたこと)がreflect.DeepEqual
を使って検証されています。
これらの変更により、encoding/json
パッケージは、JSONキーがGoの構造体のエクスポートされていないフィールド名と一致した場合に、エラーを発生させることなくそのフィールドをスキップする、より柔軟でGoの慣習に沿った挙動を示すようになります。
関連リンク
- Go言語の
encoding/json
パッケージ公式ドキュメント: https://pkg.go.dev/encoding/json - Go言語のリフレクション (
reflect
パッケージ) 公式ドキュメント: https://pkg.go.dev/reflect
参考にした情報源リンク
- Go言語におけるエクスポートされたフィールドとエクスポートされていないフィールドに関する解説記事 (一般的なGoのドキュメントやチュートリアル)
encoding/json
の挙動に関するStack Overflowの議論など- GoのIssueトラッカー (Issue #4660の具体的な内容については、古いIssueのため直接参照できませんでしたが、一般的なGoのIssueの検索方法を参考にしました)