[インデックス 13001] ファイルの概要
このコミットは、Go言語の標準ライブラリである encoding/json
パッケージにおけるJSONデコードの挙動を修正するものです。具体的には、Goの構造体フィールドにJSONタグが指定されている場合、そのタグ名がJSONキーと一致しない限り、フィールド名自体でのマッチングを行わないように変更されました。これにより、JSONタグが意図しないフィールド名とのマッチングを引き起こすバグが修正されています。
コミット
commit c3c8e35af25d99f5cfab70157e26a13b93a77e7f
Author: David Symonds <dsymonds@golang.org>
Date: Tue May 1 11:37:44 2012 +1000
encoding/json: don't match field name if a JSON struct tag is present.
Fixes #3566.
R=rsc
CC=golang-dev
https://golang.org/cl/6139048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c3c8e35af25d99f5cfab70157e26a13b93a77e7f
元コミット内容
encoding/json: don't match field name if a JSON struct tag is present.
Fixes #3566.
R=rsc
CC=golang-dev
https://golang.org/cl/6139048
変更の背景
この変更は、Goの encoding/json
パッケージがJSONデータをGoの構造体にデコードする際の、フィールドマッチングのロジックに関するバグ(Issue #3566)を修正するために行われました。
従来の encoding/json
パッケージでは、JSONオブジェクトのキーをGo構造体のフィールドにマッピングする際、以下の優先順位でマッチングを試みていました。
- JSONタグによるマッチング: 構造体フィールドに
json:"tag_name"
のようなJSONタグが指定されている場合、まずそのtag_name
とJSONキーが一致するかを試みます。 - フィールド名によるマッチング: JSONタグが存在しない、またはJSONタグが一致しなかった場合、Go構造体のフィールド名とJSONキーが一致するかを試みます(大文字小文字を区別しないマッチングも含む)。
この優先順位付け自体は正しいのですが、問題は「JSONタグが存在するにもかかわらず、そのタグ名がJSONキーと一致しなかった場合に、フォールバックとしてフィールド名でのマッチングを試みてしまう」という挙動にありました。
例えば、以下のようなGo構造体とJSONデータがあったとします。
type MyStruct struct {
Alphabet string `json:"alpha"`
}
{
"alphabet": "xyz",
"alpha": "abc"
}
この場合、開発者の意図としては、Alphabet
フィールドにはJSONタグ alpha
が指定されているため、JSONキー "alpha"
の値 "abc"
がデコードされることを期待します。しかし、従来のバグのある実装では、まずJSONキー "alphabet"
が処理され、MyStruct
の Alphabet
フィールドにJSONタグ alpha
があるにもかかわらず、フィールド名 Alphabet
とJSONキー alphabet
が(大文字小文字を無視して)一致すると判断され、誤って "xyz"
がデコードされてしまう可能性がありました。その後、JSONキー "alpha"
が処理された際に、既に Alphabet
フィールドに値が設定されているため、その値が上書きされることはありませんでした。
この挙動は、JSONタグを明示的に指定することで、フィールド名との偶発的な衝突を避け、より厳密なマッピングを意図している開発者の期待に反するものでした。このバグにより、予期せぬデータが構造体にデコードされ、アプリケーションの誤動作につながる可能性がありました。
このコミットは、この問題を解決し、JSONタグが指定されている場合は、そのタグ名がJSONキーと一致しない限り、フィールド名でのマッチングを完全にスキップするように変更することで、より予測可能で堅牢なJSONデコードを実現しています。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびJSONに関する基本的な知識が必要です。
-
Go言語の構造体 (Structs): Go言語の構造体は、異なる型のフィールドをまとめた複合データ型です。JSONデータをGoの構造体にデコードする際、JSONオブジェクトのキーと構造体のフィールドが対応付けられます。
-
Go言語の構造体タグ (Struct Tags): Goの構造体フィールドには、
json:"key_name"
のような「タグ」を付与することができます。これは、リフレクションAPIを通じてアクセスできるメタデータであり、encoding/json
パッケージでは、JSONデータと構造体フィールドのマッピングを制御するために広く利用されます。json:"key_name"
: JSONデータ内のkey_name
というキーを、このフィールドにマッピングします。json:"-"
: このフィールドはJSONのエンコード/デコードから無視されます。json:"key_name,omitempty"
:key_name
にマッピングし、フィールドがゼロ値(数値の0、文字列の""、スライスのnilなど)の場合はJSON出力から省略します。
-
encoding/json
パッケージ: Goの標準ライブラリに含まれるパッケージで、JSONデータとGoのデータ構造(構造体、マップ、スライスなど)間のエンコード(Marshal)およびデコード(Unmarshal)機能を提供します。json.Unmarshal(data []byte, v interface{}) error
: JSONバイトスライスdata
をGoの値v
にデコードします。json.Marshal(v interface{}) ([]byte, error)
: Goの値v
をJSONバイトスライスにエンコードします。
-
JSON (JavaScript Object Notation): 軽量なデータ交換フォーマットです。キーと値のペアの集まり(オブジェクト)や、値の順序付きリスト(配列)で構成されます。
-
リフレクション (Reflection): Goのリフレクションは、プログラムの実行時に型情報にアクセスしたり、値を操作したりする機能です。
encoding/json
パッケージは、リフレクションを使用して構造体のフィールドやタグ情報を動的に読み取り、JSONデコードを行います。
このコミットの文脈では、encoding/json
パッケージがJSONオブジェクトのキーをGo構造体のフィールドにマッピングする際の内部ロジック、特に構造体タグがどのようにフィールドマッチングに影響を与えるかが重要になります。
技術的詳細
このコミットの技術的詳細は、encoding/json
パッケージのデコード処理、特にJSONオブジェクトのキーとGo構造体フィールドのマッピングロジックに焦点を当てています。
encoding/json
パッケージは、JSONオブジェクトをGo構造体にデコードする際、リフレクションを使用して構造体のフィールドを走査し、対応するJSONキーを探します。このマッチングプロセスは、以下の優先順位で行われます。
-
JSONタグによるマッチング: 構造体フィールドに
json:"tag_name"
のようなJSONタグが指定されている場合、デコーダはまずこのtag_name
をJSONキーとして探します。 -
フィールド名によるマッチング: JSONタグが指定されていない場合、またはJSONタグが指定されていてもそのタグ名がJSONキーと一致しなかった場合、デコーダはGo構造体のフィールド名をJSONキーとして探します。この際、フィールド名とJSONキーの大文字小文字を区別しないマッチングも考慮されます(例:
FieldName
とfieldname
)。
このコミット以前のバグは、上記2番目のステップにおいて、JSONタグが明示的に指定されているにもかかわらず、そのタグ名がJSONキーと一致しなかった場合に、誤ってフィールド名によるマッチングを試みてしまうという点にありました。
修正後のロジックは、src/pkg/encoding/json/decode.go
の decodeState.object
メソッド内で変更されています。このメソッドは、JSONオブジェクトをデコードし、Go構造体のフィールドに値を割り当てる主要なロジックを含んでいます。
変更点の中核は、JSONタグが存在するかどうかを tagName != ""
で確認し、もしタグが存在するならば、そのタグ名がJSONキーと一致した場合のみフィールドを割り当て、一致しなかった場合は直ちに次のフィールドの処理に移る (continue
) という点です。これにより、JSONタグが指定されているフィールドに対しては、フィールド名によるフォールバックマッチングが完全に抑制されます。
この修正により、開発者がJSONタグを使用して明示的なマッピングを意図した場合に、フィールド名との偶発的な一致によって予期せぬデコードが行われるというバグが解消されました。これは、encoding/json
パッケージの挙動をより予測可能にし、開発者の意図に沿ったものにするための重要な改善です。
コアとなるコードの変更箇所
変更は主に src/pkg/encoding/json/decode.go
と src/pkg/encoding/json/decode_test.go
の2つのファイルで行われています。
src/pkg/encoding/json/decode.go
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -504,10 +504,15 @@ func (d *decodeState) object(v reflect.Value) {
// First, tag match
tagName, _ := parseTag(tag)
- if tagName == key {
- f = sf
- ok = true
- break // no better match possible
+ if tagName != "" {
+ if tagName == key {
+ f = sf
+ ok = true
+ break // no better match possible
+ }
+ // There was a tag, but it didn't match.
+ // Ignore field names.
+ continue
}
// Second, exact field name match
if sf.Name == key {
src/pkg/encoding/json/decode_test.go
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -18,6 +18,10 @@ type T struct {
Z int `json:"-"`
}\n
+type U struct {
+ Alphabet string `json:"alpha"`
+}\n
+
type tx struct {
x int
}\n
@@ -72,6 +76,10 @@ var unmarshalTests = []unmarshalTest{
// Z has a "-" tag.
{`{"Y": 1, "Z": 2}`, new(T), T{Y: 1}, nil},\n
+\t{`{"alpha": "abc", "alphabet": "xyz"}`, new(U), U{Alphabet: "abc"}, nil},\n
+\t{`{"alpha": "abc"}`, new(U), U{Alphabet: "abc"}, nil},\n
+\t{`{"alphabet": "xyz"}`, new(U), U{}, nil},\n
+\n
// syntax errors
{`{"X": "foo", "Y"}`, nil, nil, &SyntaxError{"invalid character '}' after object key", 17}},\n
{`[1, 2, 3+]`, nil, nil, &SyntaxError{"invalid character '+' after array element", 9}},\n
コアとなるコードの解説
src/pkg/encoding/json/decode.go
の変更点
decodeState.object
メソッドは、JSONオブジェクトをGo構造体にデコードする際の中心的なロジックを含んでいます。このメソッド内で、JSONキーと構造体フィールドのマッチングが行われます。
変更前のコードは以下のようになっていました。
// First, tag match
tagName, _ := parseTag(tag)
if tagName == key {
f = sf
ok = true
break // no better match possible
}
// Second, exact field name match
if sf.Name == key {
// ...
}
このコードでは、tagName == key
が true
の場合(JSONタグがJSONキーと一致した場合)は、そのフィールドにデコードしてループを抜けます。しかし、tagName == key
が false
の場合(JSONタグがJSONキーと一致しなかった場合)は、そのまま次の if sf.Name == key
のチェックに進んでしまい、フィールド名によるマッチングを試みていました。これがバグの原因でした。
変更後のコードは以下のようになっています。
// First, tag match
tagName, _ := parseTag(tag)
if tagName != "" { // JSONタグが存在する場合
if tagName == key { // JSONタグがJSONキーと一致した場合
f = sf
ok = true
break // これ以上良いマッチングは不可能なのでループを抜ける
}
// There was a tag, but it didn't match.
// Ignore field names.
continue // タグは存在したが一致しなかった。フィールド名でのマッチングは無視し、次のフィールドへ
}
// Second, exact field name match (JSONタグが存在しない場合のみ実行される)
if sf.Name == key {
// ...
}
この変更のポイントは、if tagName != ""
という条件が追加されたことです。
tagName != ""
がfalse
の場合(つまり、JSONタグが構造体フィールドに存在しない場合)は、以前と同様にif sf.Name == key
のチェックに進み、フィールド名によるマッチングが行われます。これは正しい挙動です。tagName != ""
がtrue
の場合(つまり、JSONタグが構造体フィールドに存在する場合)は、さらにif tagName == key
のチェックが行われます。tagName == key
がtrue
の場合(JSONタグがJSONキーと一致した場合)は、フィールドが特定され、break
でループを抜けます。tagName == key
がfalse
の場合(JSONタグは存在するがJSONキーと一致しなかった場合)は、continue
が実行されます。これにより、フィールド名によるマッチングのセクションが完全にスキップされ、次の構造体フィールドの処理に移ります。
この修正により、JSONタグが明示的に指定されているフィールドに対しては、そのタグ名がJSONキーと一致しない限り、フィールド名による偶発的なマッチングが起こらなくなりました。
src/pkg/encoding/json/decode_test.go
の変更点
新しいテストケースが unmarshalTests
スライスに追加されています。
type U struct {
Alphabet string `json:"alpha"`
}
この U
構造体は、Alphabet
というフィールド名と、json:"alpha"
というJSONタグを持っています。
追加されたテストケースは以下の3つです。
-
{
{"alpha": "abc", "alphabet": "xyz"}, new(U), U{Alphabet: "abc"}, nil},
- JSONデータには
alpha
とalphabet
の両方のキーが含まれています。 - 期待される結果は
U{Alphabet: "abc"}
です。これは、Alphabet
フィールドのJSONタグalpha
が優先され、JSONキー"alpha"
の値"abc"
がデコードされることを確認します。修正前は、alphabet
の値がデコードされる可能性がありました。
- JSONデータには
-
{
{"alpha": "abc"}, new(U), U{Alphabet: "abc"}, nil},
- JSONデータには
alpha
キーのみが含まれています。 - 期待される結果は
U{Alphabet: "abc"}
です。これは、JSONタグによる通常のデコードが正しく機能することを確認します。
- JSONデータには
-
{
{"alphabet": "xyz"}, new(U), U{}, nil},
- JSONデータには
alphabet
キーのみが含まれています。 - 期待される結果は
U{}
です。これは、Alphabet
フィールドにjson:"alpha"
タグが存在するため、JSONキー"alphabet"
がフィールド名と一致したとしても、デコードされないことを確認します。修正前は、Alphabet
フィールドに"xyz"
がデコードされてしまう可能性がありました。
- JSONデータには
これらのテストケースは、encoding/json
パッケージがJSONタグの存在を正しく認識し、タグが指定されている場合はフィールド名によるフォールバックマッチングを行わないという、新しい(そして正しい)挙動を検証しています。
関連リンク
- Go Issue #3566: encoding/json: don't match field name if a JSON struct tag is present
- Go CL 6139048: encoding/json: don't match field name if a JSON struct tag is present.
参考にした情報源リンク
- Go言語の公式ドキュメント: The Go Programming Language Specification - Struct types
- Go言語の公式ドキュメント: The Go Programming Language Specification - Struct tags
- Go言語の
encoding/json
パッケージドキュメント: pkg.go.dev/encoding/json - Go言語のリフレクションに関するブログ記事やチュートリアル (例: Go Reflection Tutorial)