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

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

このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおける、構造体のフィールドをJSONにエンコードする際の挙動、特にタグ付きフィールドと埋め込み構造体によるフィールドの「シャドーイング(shadowing)」に関する決定ロジックを修正するものです。以前のテストでは不十分であったため、新しいテストケースが追加され、タグ付きフィールドが他のフィールドに対してどのように優先されるか、および競合するフィールドがどのように処理されるかが明確化されています。

コミット

  • コミットハッシュ: 5fd708c000e50bac6091662c53c79a469a43071a
  • 作者: Rob Pike r@golang.org
  • 日付: Wed Apr 10 13:05:34 2013 -0700

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/5fd708c000e50bac6091662c53c79a469a43071a

元コミット内容

encoding/json: different decision on tags and shadowing
If there are no tags, the rules are the same as before.
If there is a tagged field, choose it if there is exactly one
at the top level of all fields.
More tests. The old tests were clearly inadequate, since
they all pass as is. The new tests only work with the new code.

R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/8617044

変更の背景

Goの encoding/json パッケージは、Goの構造体をJSON形式に変換(マーシャリング)する際に、構造体のフィールド名やJSONタグ(json:"fieldName")を考慮して処理を行います。特に、Goの構造体埋め込み(struct embedding)機能を使用すると、複数の埋め込み構造体が同じ名前のフィールドを持つ場合があり、どのフィールドをJSON出力に含めるべきかという「シャドーイング」の問題が発生します。

このコミット以前は、encoding/json のフィールド選択ロジックが、タグ付きフィールドの優先順位や、複数の競合するフィールドが存在する場合の挙動について、曖昧さや不適切な処理を含んでいた可能性があります。コミットメッセージにある「The old tests were clearly inadequate」という記述から、既存のテストではこの問題が十分にカバーされておらず、実際の挙動が期待と異なるケースがあったことが示唆されます。

この変更の目的は、JSONタグを持つフィールドの優先順位を明確にし、特に複数のフィールドが競合する場合に、タグ付きフィールドが適切に「支配的(dominant)」なフィールドとして選択されるように、または競合が解決できない場合に適切に無視されるように、ロジックを修正することです。これにより、encoding/json のマーシャリング挙動の一貫性と予測可能性が向上します。

前提知識の解説

  1. Goの encoding/json パッケージ: Go言語でJSONデータを扱うための標準ライブラリです。json.Marshal 関数はGoのデータ構造(構造体、マップ、スライスなど)をJSONバイト列に変換し、json.Unmarshal 関数はその逆を行います。

  2. Goの構造体埋め込み (Struct Embedding): Goでは、ある構造体の中に別の構造体をフィールド名なしで宣言することで、その埋め込まれた構造体のフィールドやメソッドを外側の構造体が「継承」したかのように振る舞わせることができます。これにより、コードの再利用性が高まります。 例:

    type Person struct {
        Name string
        Age  int
    }
    
    type Employee struct {
        Person // Person構造体を埋め込み
        ID     string
    }
    

    この場合、Employee のインスタンスは NameAge フィールドに直接アクセスできます。

  3. JSONタグ (Struct Tags): Goの構造体フィールドには、バッククォートで囲まれた文字列として「タグ」を付与できます。encoding/json パッケージは、このタグを利用してJSONエンコード/デコードの挙動をカスタマイズします。最も一般的なのは json:"fieldName" で、JSON出力時のフィールド名を指定します。 例:

    type User struct {
        Name string `json:"user_name"` // JSONでは "user_name" として出力される
        Age  int    `json:"-"`         // JSON出力からこのフィールドを除外する
    }
    
  4. フィールドのシャドーイングと競合: 構造体埋め込みを使用すると、異なる埋め込み構造体や、外側の構造体自身が同じ名前のフィールドを持つことがあります。 例:

    type A struct { S string }
    type B struct { S string }
    type C struct {
        A
        B
    }
    

    この C 構造体では、A.SB.S の両方が存在し、C.S と直接アクセスしようとすると曖昧さ(ambiguity)が生じます。encoding/json は、このような競合を解決するための内部ルールを持っています。一般的に、より「浅い」レベル(埋め込みが少ない)のフィールドが優先されるか、タグ付きフィールドが優先されるか、あるいは競合が解決できない場合はそのフィールドが無視されることがあります。

  5. reflect パッケージ: Goの reflect パッケージは、実行時に型情報を検査し、値の操作を可能にします。encoding/json パッケージは、構造体のフィールドを動的に検査し、JSONタグを読み取り、値を抽出するために reflect パッケージを extensively に使用しています。

技術的詳細

このコミットの主要な変更は、src/pkg/encoding/json/encode.go 内の typeFields 関数と dominantField 関数に集中しています。これらの関数は、Goの構造体からJSONエンコード対象となるフィールドを決定するロジックを担っています。

変更前のロジック(推測)

変更前の dominantField 関数は、hasTags というブール値の引数を受け取っていました。これは、処理対象のフィールド群の中にJSONタグを持つものが存在するかどうかを示していたと考えられます。もしタグが存在する場合、タグを持たないフィールドを削除し、タグ付きフィールドを優先するようなロジックが含まれていたようです。しかし、複数のタグ付きフィールドが競合する場合や、タグがない場合の競合解決ルールが不十分であった可能性があります。

変更後のロジック

  1. typeFields 関数の変更: typeFields 関数内で dominantField を呼び出す際、hasTags 引数が削除されました。これは、dominantField 関数自体がタグの有無を内部で判断するようになったためです。

  2. dominantField 関数の大幅な変更: dominantField 関数は、フィールドのリスト fields []field を受け取り、その中から「支配的」なフィールドを一つ選択するか、競合により選択できない場合は (field{}, false) を返します。

    新しいロジックの主要なポイントは以下の通りです。

    • 最短のインデックス長を持つフィールドの優先: fields スライスは、フィールドの index(構造体埋め込みの深さを示すスライス)の長さが短い順にソートされています。これは、より「浅い」レベルで定義されたフィールドがリストの先頭に来ることを意味します。 新しいロジックでは、まず fields[0]index の長さを基準とし、それよりも長い index を持つフィールドはすべて無視されます(スライスを切り詰めます)。これにより、最も浅いレベルのフィールドのみが考慮対象となります。

    • タグ付きフィールドの優先と競合検出: 残ったフィールドの中から、f.tag(JSONタグの有無を示すブール値)をチェックし、タグ付きフィールドを探します。

      • もしタグ付きフィールドが複数見つかった場合(tagged >= 0 かつ f.tag が再度 true になる場合)、これは同じレベルで複数のタグ付きフィールドが競合していることを意味します。この場合、return field{}, false を返し、フィールドは選択されません(JSON出力から除外されます)。
      • タグ付きフィールドが一つだけ見つかった場合、そのフィールドが「支配的」なフィールドとして選択され、return fields[tagged], true で返されます。
    • タグなしフィールドの競合検出: もしタグ付きフィールドが一つも見つからなかった場合、残りのすべてのフィールドは同じインデックス長(つまり同じレベル)を持ち、かつタグがないことになります。

      • この状態で len(fields) > 1 であれば、同じレベルで複数のタグなしフィールドが競合していることになります。この場合も return field{}, false を返し、フィールドは選択されません。
      • len(fields) == 1 であれば、競合するフィールドがないため、その唯一のフィールドが fields[0] として返されます。

この新しいロジックにより、JSONタグを持つフィールドが明確に優先され、複数のタグ付きフィールドが競合する場合や、タグなしフィールドが競合する場合の挙動がより厳密に定義されました。

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

src/pkg/encoding/json/encode.go

--- a/src/pkg/encoding/json/encode.go
+++ b/src/pkg/encoding/json/encode.go
@@ -666,19 +666,17 @@ func typeFields(t reflect.Type) []field {
 		// Find the sequence of fields with the name of this first field.
 		fi := fields[i]
 		name := fi.name
-		hasTags := fi.tag
 		for advance = 1; i+advance < len(fields); advance++ {
 			fj := fields[i+advance]
 			if fj.name != name {
 				break
 			}
-			hasTags = hasTags || fj.tag
 		}
 		if advance == 1 { // Only one field with this name
 			out = append(out, fi)
 			continue
 		}
-		dominant, ok := dominantField(fields[i:i+advance], hasTags)
+		dominant, ok := dominantField(fields[i : i+advance])
 		if ok {
 			out = append(out, dominant)
 		}
@@ -696,23 +694,33 @@ func typeFields(t reflect.Type) []field {
 // JSON tags. If there are multiple top-level fields, the boolean
 // will be false: This condition is an error in Go and we skip all
 // the fields.
-func dominantField(fields []field, hasTags bool) (field, bool) {
-	if hasTags {
-		// If there's a tag, it gets promoted, so delete all fields without tags.
-		var j int
-		for i := 0; i < len(fields); i++ {
-			if fields[i].tag {
-				fields[j] = fields[i]
-				j++
+func dominantField(fields []field) (field, bool) {
+	// The fields are sorted in increasing index-length order. The winner
+	// must therefore be one with the shortest index length. Drop all
+	// longer entries, which is easy: just truncate the slice.
+	length := len(fields[0].index)
+	tagged := -1 // Index of first tagged field.
+	for i, f := range fields {
+		if len(f.index) > length {
+			fields = fields[:i]
+			break
+		}
+		if f.tag {
+			if tagged >= 0 {
+				// Multiple tagged fields at the same level: conflict.
+				// Return no field.
+				return field{}, false
 			}
+			tagged = i
 		}
-		fields = fields[:j]
 	}
-	// The fields are sorted in increasing index-length order. The first entry
-	// therefore wins, unless the second entry is of the same length. If that
-	// is true, then there is a conflict (two fields named "X" at the same level)
-	// and we have no fields.
-	if len(fields) > 1 && len(fields[0].index) == len(fields[1].index) {
+	if tagged >= 0 {
+		return fields[tagged], true
+	}
+	// All remaining fields have the same length. If there's more than one,
+	// we have a conflict (two fields named "X" at the same level) and we
+	// return no field.
+	if len(fields) > 1 {
 		return field{}, false
 	}
 	return fields[0], true

src/pkg/encoding/json/encode_test.go

--- a/src/pkg/encoding/json/encode_test.go
+++ b/src/pkg/encoding/json/encode_test.go
@@ -221,7 +221,7 @@ type BugC struct {
 }
 
 // Legal Go: We never use the repeated embedded field (S).
-type BugD struct {
+type BugX struct {
 	A int
 	BugA
 	BugB
@@ -243,7 +243,7 @@ func TestEmbeddedBug(t *testing.T) {
 		t.Fatalf("Marshal: got %s want %s", got, want)
 	}
 	// Now check that the duplicate field, S, does not appear.
-	x := BugD{
+	x := BugX{
 		A: 23,
 	}
 	b, err = Marshal(x)
@@ -256,3 +256,57 @@ func TestEmbeddedBug(t *testing.T) {
 		t.Fatalf("Marshal: got %s want %s", got, want)
 	}
 }
+
+type BugD struct { // Same as BugA after tagging.
+	XXX string `json:"S"`
+}
+
+// BugD's tagged S field should dominate BugA's.
+type BugY struct {
+	BugA
+	BugD
+}
+
+// Test that a field with a tag dominates untagged fields.
+func TestTaggedFieldDominates(t *testing.T) {
+	v := BugY{
+		BugA{"BugA"},
+		BugD{"BugD"},
+	}
+	b, err := Marshal(v)
+	if err != nil {
+		t.Fatal("Marshal:", err)
+	}
+	want := `{"S":"BugD"}`
+	got := string(b)
+	if got != want {
+		t.Fatalf("Marshal: got %s want %s", got, want)
+	}
+}
+
+// There are no tags here, so S should not appear.
+type BugZ struct {
+	BugA
+	BugC
+	BugY // Contains a tagged S field through BugD; should not dominate.
+}
+
+func TestDuplicatedFieldDisappears(t *testing.T) {
+	v := BugZ{
+		BugA{"BugA"},
+		BugC{"BugC"},
+		BugY{
+			BugA{"nested BugA"},
+			BugD{"nested BugD"},
+		},
+	}
+	b, err := Marshal(v)
+	if err != nil {
+		t.Fatal("Marshal:", err)
+	}
+	want := `{}`
+	got := string(b)
+	if got != want {
+		t.Fatalf("Marshal: got %s want %s", got, want)
+	}
+}

コアとなるコードの解説

encode.go の変更

  • typeFields 関数: この関数は、与えられた reflect.Type から、JSONエンコードの対象となるフィールドのリストを構築します。変更点としては、dominantField 関数を呼び出す際に、以前は hasTags というブール値を渡していましたが、これが削除されました。これは、dominantField 関数自体が、フィールドリスト内のタグの有無を判断する責任を持つようになったためです。これにより、関数のインターフェースが簡素化され、ロジックが dominantField に集約されました。

  • dominantField 関数: この関数は、同じ名前を持つ複数のフィールド(通常は構造体埋め込みによって発生する競合)の中から、JSONエンコード時に実際に使用される「支配的」なフィールドを決定します。

    1. 最短インデックス長の優先: length := len(fields[0].index)if len(f.index) > length のロジックは、Goの構造体埋め込みにおいて、より「浅い」レベル(つまり、埋め込みの階層が少ない)で定義されたフィールドを優先するという原則を実装しています。fields スライスは既にインデックス長でソートされているため、最初のフィールドのインデックス長が最も短く、それより長いインデックスを持つフィールドは、たとえ同じ名前であっても考慮対象から外されます。

    2. タグ付きフィールドの優先: tagged := -1if f.tag { ... tagged = i } のロジックは、JSONタグを持つフィールドを優先するためのものです。

      • もし、考慮対象のフィールドの中にタグ付きフィールドが一つだけ存在する場合、そのフィールドが最も優先され、JSON出力に採用されます。
      • しかし、同じレベル(最短インデックス長)で複数のタグ付きフィールドが存在する場合(if tagged >= 0 { ... return field{}, false })、これは解決できない競合とみなされ、どのフィールドも選択されず、JSON出力には含まれません。これは、曖昧さを排除し、予測可能な挙動を保証するための重要な変更です。
    3. タグなしフィールドの競合: タグ付きフィールドが一つも存在しない場合、残りのフィールドはすべてタグなしで、かつ同じインデックス長を持つことになります。

      • この状態で if len(fields) > 1 { return field{}, false } のロジックが適用されます。これは、同じレベルで複数のタグなしフィールドが競合している場合、それらも解決できない競合とみなし、JSON出力から除外するというルールです。
      • もし残ったフィールドが一つだけであれば、それが唯一の選択肢として採用されます。

この変更により、encoding/json は、タグ付きフィールドの優先順位を明確にし、競合解決のルールをより厳密に適用するようになりました。

encode_test.go の変更

テストファイルでは、新しい挙動を検証するための複数のテストケースが追加されています。

  • BugX へのリネーム: 既存のテストケース BugDBugX にリネームされています。これは、新しいテストケースで BugD という名前を再利用するためです。

  • BugD 構造体の追加: type BugD struct { XXX string json:"S" } この新しい BugD は、XXX というフィールド名を持ちながら、json:"S" というタグによってJSON上では S という名前で出力されるように定義されています。これは、既存の BugA 構造体(S string フィールドを持つ)と競合する可能性のあるフィールドを意図的に作成しています。

  • BugY 構造体の追加: type BugY struct { BugA; BugD } BugYBugA と新しい BugD を埋め込んでいます。両者ともJSON上では S という名前で競合する可能性がありますが、BugDXXX フィールドには json:"S" というタグが付いています。

  • TestTaggedFieldDominates テスト: このテストは、BugY をマーシャリングした際に、BugD のタグ付き S フィールドが BugA のタグなし S フィールドよりも優先されることを検証します。期待される出力は {"S":"BugD"} であり、これは新しい dominantField のロジックが正しく機能していることを示します。

  • BugZ 構造体の追加: type BugZ struct { BugA; BugC; BugY } BugZBugA, BugC, そして BugY を埋め込んでいます。BugY はさらに BugABugD を埋め込んでいるため、S という名前のフィールドが複数の経路から現れる可能性があります。

  • TestDuplicatedFieldDisappears テスト: このテストは、BugZ をマーシャリングした際に、S という名前のフィールドがJSON出力に現れないことを検証します。これは、複数の競合するフィールドが存在し、その中に支配的なタグ付きフィールドが一つに特定できない場合(またはタグ付きフィールドがない場合)に、そのフィールドがJSON出力から完全に除外されるという新しいロジックの挙動をテストしています。期待される出力は {} であり、これは競合する S フィールドが適切に無視されたことを示します。

これらの新しいテストは、以前のテストではカバーされていなかった、タグ付きフィールドの優先順位と、複数のフィールドが競合する場合の厳密な挙動を検証するために不可欠です。

関連リンク

参考にした情報源リンク

  • Go言語の encoding/json パッケージに関する公式ドキュメント
  • Go言語の構造体埋め込みに関する公式ドキュメント
  • Go言語の reflect パッケージに関する公式ドキュメント
  • Go言語の構造体タグに関する一般的な情報源