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

[インデックス 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構造体のフィールドにマッピングする際、以下の優先順位でマッチングを試みていました。

  1. JSONタグによるマッチング: 構造体フィールドに json:"tag_name" のようなJSONタグが指定されている場合、まずその tag_name とJSONキーが一致するかを試みます。
  2. フィールド名によるマッチング: 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" が処理され、MyStructAlphabet フィールドにJSONタグ alpha があるにもかかわらず、フィールド名 Alphabet とJSONキー alphabet が(大文字小文字を無視して)一致すると判断され、誤って "xyz" がデコードされてしまう可能性がありました。その後、JSONキー "alpha" が処理された際に、既に Alphabet フィールドに値が設定されているため、その値が上書きされることはありませんでした。

この挙動は、JSONタグを明示的に指定することで、フィールド名との偶発的な衝突を避け、より厳密なマッピングを意図している開発者の期待に反するものでした。このバグにより、予期せぬデータが構造体にデコードされ、アプリケーションの誤動作につながる可能性がありました。

このコミットは、この問題を解決し、JSONタグが指定されている場合は、そのタグ名がJSONキーと一致しない限り、フィールド名でのマッチングを完全にスキップするように変更することで、より予測可能で堅牢なJSONデコードを実現しています。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびJSONに関する基本的な知識が必要です。

  1. Go言語の構造体 (Structs): Go言語の構造体は、異なる型のフィールドをまとめた複合データ型です。JSONデータをGoの構造体にデコードする際、JSONオブジェクトのキーと構造体のフィールドが対応付けられます。

  2. 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出力から省略します。
  3. 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バイトスライスにエンコードします。
  4. JSON (JavaScript Object Notation): 軽量なデータ交換フォーマットです。キーと値のペアの集まり(オブジェクト)や、値の順序付きリスト(配列)で構成されます。

  5. リフレクション (Reflection): Goのリフレクションは、プログラムの実行時に型情報にアクセスしたり、値を操作したりする機能です。encoding/json パッケージは、リフレクションを使用して構造体のフィールドやタグ情報を動的に読み取り、JSONデコードを行います。

このコミットの文脈では、encoding/json パッケージがJSONオブジェクトのキーをGo構造体のフィールドにマッピングする際の内部ロジック、特に構造体タグがどのようにフィールドマッチングに影響を与えるかが重要になります。

技術的詳細

このコミットの技術的詳細は、encoding/json パッケージのデコード処理、特にJSONオブジェクトのキーとGo構造体フィールドのマッピングロジックに焦点を当てています。

encoding/json パッケージは、JSONオブジェクトをGo構造体にデコードする際、リフレクションを使用して構造体のフィールドを走査し、対応するJSONキーを探します。このマッチングプロセスは、以下の優先順位で行われます。

  1. JSONタグによるマッチング: 構造体フィールドに json:"tag_name" のようなJSONタグが指定されている場合、デコーダはまずこの tag_name をJSONキーとして探します。

  2. フィールド名によるマッチング: JSONタグが指定されていない場合、またはJSONタグが指定されていてもそのタグ名がJSONキーと一致しなかった場合、デコーダはGo構造体のフィールド名をJSONキーとして探します。この際、フィールド名とJSONキーの大文字小文字を区別しないマッチングも考慮されます(例: FieldNamefieldname)。

このコミット以前のバグは、上記2番目のステップにおいて、JSONタグが明示的に指定されているにもかかわらず、そのタグ名がJSONキーと一致しなかった場合に、誤ってフィールド名によるマッチングを試みてしまうという点にありました。

修正後のロジックは、src/pkg/encoding/json/decode.godecodeState.object メソッド内で変更されています。このメソッドは、JSONオブジェクトをデコードし、Go構造体のフィールドに値を割り当てる主要なロジックを含んでいます。

変更点の中核は、JSONタグが存在するかどうかを tagName != "" で確認し、もしタグが存在するならば、そのタグ名がJSONキーと一致した場合のみフィールドを割り当て、一致しなかった場合は直ちに次のフィールドの処理に移る (continue) という点です。これにより、JSONタグが指定されているフィールドに対しては、フィールド名によるフォールバックマッチングが完全に抑制されます。

この修正により、開発者がJSONタグを使用して明示的なマッピングを意図した場合に、フィールド名との偶発的な一致によって予期せぬデコードが行われるというバグが解消されました。これは、encoding/json パッケージの挙動をより予測可能にし、開発者の意図に沿ったものにするための重要な改善です。

コアとなるコードの変更箇所

変更は主に src/pkg/encoding/json/decode.gosrc/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 == keytrue の場合(JSONタグがJSONキーと一致した場合)は、そのフィールドにデコードしてループを抜けます。しかし、tagName == keyfalse の場合(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 != "" という条件が追加されたことです。

  1. tagName != ""false の場合(つまり、JSONタグが構造体フィールドに存在しない場合)は、以前と同様に if sf.Name == key のチェックに進み、フィールド名によるマッチングが行われます。これは正しい挙動です。
  2. tagName != ""true の場合(つまり、JSONタグが構造体フィールドに存在する場合)は、さらに if tagName == key のチェックが行われます。
    • tagName == keytrue の場合(JSONタグがJSONキーと一致した場合)は、フィールドが特定され、break でループを抜けます。
    • tagName == keyfalse の場合(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つです。

  1. {{"alpha": "abc", "alphabet": "xyz"}, new(U), U{Alphabet: "abc"}, nil},

    • JSONデータには alphaalphabet の両方のキーが含まれています。
    • 期待される結果は U{Alphabet: "abc"} です。これは、Alphabet フィールドのJSONタグ alpha が優先され、JSONキー "alpha" の値 "abc" がデコードされることを確認します。修正前は、alphabet の値がデコードされる可能性がありました。
  2. {{"alpha": "abc"}, new(U), U{Alphabet: "abc"}, nil},

    • JSONデータには alpha キーのみが含まれています。
    • 期待される結果は U{Alphabet: "abc"} です。これは、JSONタグによる通常のデコードが正しく機能することを確認します。
  3. {{"alphabet": "xyz"}, new(U), U{}, nil},

    • JSONデータには alphabet キーのみが含まれています。
    • 期待される結果は U{} です。これは、Alphabet フィールドに json:"alpha" タグが存在するため、JSONキー "alphabet" がフィールド名と一致したとしても、デコードされないことを確認します。修正前は、Alphabet フィールドに "xyz" がデコードされてしまう可能性がありました。

これらのテストケースは、encoding/json パッケージがJSONタグの存在を正しく認識し、タグが指定されている場合はフィールド名によるフォールバックマッチングを行わないという、新しい(そして正しい)挙動を検証しています。

関連リンク

参考にした情報源リンク