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

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

このコミットは、Go言語の標準ライブラリ encoding/json パッケージに、JSONのアンマーシャリング(デコード)およびマーシャリング(エンコード)時に、構造体で定義されていない余分なフィールドを処理するための新しい構造体タグオプション "overflow" を追加するものです。これにより、Goの構造体とJSONデータ間のマッピングの柔軟性が向上し、特に未知のフィールドを含むJSONデータを扱う際に便利になります。

コミット

commit 466001d05d366cbc97edfb65dc6f5cb883df0498
Author: Andrew Gerrand <adg@golang.org>
Date:   Thu Aug 29 14:39:55 2013 +1000

    encoding/json: add "overflow" struct tag option
    
    Fixes #6213.
    
    R=golang-dev, dsymonds, bradfitz
    CC=golang-dev
    https://golang.org/cl/13180043

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

https://github.com/golang/go/commit/466001d05d366cbc97edfb65dc6f5cb883df0498

元コミット内容

encoding/json: add "overflow" struct tag option

このコミットは、encoding/json パッケージに「overflow」という構造体タグオプションを追加します。これは、構造体で定義されていないJSONフィールドをマップ型のフィールドに格納できるようにするためのものです。

変更の背景

Goの encoding/json パッケージは、Goの構造体とJSONデータ間のマッピングを自動的に行います。しかし、これまでの実装では、JSONデータに構造体で定義されていないフィールドが含まれている場合、それらのフィールドは単に無視されていました。これは、APIのバージョンアップなどで新しいフィールドが追加された場合や、柔軟なデータ構造を扱いたい場合に問題となることがありました。

コミットメッセージにある Fixes #6213 は、この問題に対する解決策として、未知のJSONフィールドを特定のマップ型フィールドに「オーバーフロー」させるメカニズムを導入する必要があったことを示唆しています。これにより、開発者はJSONデータのすべての情報を失うことなく、必要なフィールドだけを構造体にマッピングし、残りを動的に処理できるようになります。

前提知識の解説

Goの encoding/json パッケージ

encoding/json パッケージは、Goのデータ構造とJSONデータの間で変換を行うための標準ライブラリです。

  • マーシャリング (Marshal): Goのデータ構造(構造体、マップ、スライスなど)をJSON形式のバイトスライスに変換するプロセスです。json.Marshal() 関数を使用します。
  • アンマーシャリング (Unmarshal): JSON形式のバイトスライスをGoのデータ構造に変換するプロセスです。json.Unmarshal() 関数を使用します。

構造体タグ (Struct Tags)

Goの構造体フィールドには、バッククォート ( ) で囲まれた文字列として「構造体タグ」を付与できます。これらのタグは、リフレクションを通じて実行時に読み取られ、JSONエンコーディング/デコーディングの挙動をカスタマイズするために広く使用されます。

例:

type User struct {
    Name string `json:"user_name"` // JSONでは "user_name" というキーにマッピング
    Age  int    `json:"-"`         // JSONへのエンコード/デコードからこのフィールドを除外
    Email string `json:"email,omitempty"` // フィールドがゼロ値の場合、JSONから省略
}

reflect パッケージ

encoding/json パッケージは、Goの reflect パッケージを内部的に利用して、実行時に構造体の型情報(フィールド名、型、タグなど)を検査し、JSONとのマッピングを行います。これにより、コンパイル時に型が不明な場合でも、汎用的なJSON処理が可能になります。

技術的詳細

このコミットの主要な変更点は、encoding/json パッケージに "overflow" という新しい構造体タグオプションを導入したことです。このオプションは、map[string]interface{} のようなマップ型の構造体フィールドに適用されます。

アンマーシャリング (デコード) 時の挙動

"overflow" タグが付与されたマップ型のフィールドが存在する場合、json.Unmarshal は以下の挙動を示します。

  1. 入力JSONオブジェクトのキーが、Goの構造体の他のフィールド名(JSONタグで指定された名前を含む)と一致するかどうかをチェックします。
  2. 一致するフィールドがあれば、通常通りそのフィールドに値をデコードします。
  3. 一致しないキーと値のペアは、"overflow" タグが付与されたマップ型のフィールドに格納されます。

これにより、JSONデータに予期せぬフィールドが含まれていても、それらの情報が失われることなく、特定のマップに集約されるようになります。

マーシャリング (エンコード) 時の挙動

"overflow" タグが付与されたマップ型のフィールドが存在する場合、json.Marshal は以下の挙動を示します。

  1. 通常通り、構造体の各フィールドをJSONオブジェクトのキーと値に変換します。
  2. "overflow" タグが付与されたマップ型のフィールドの内容は、そのマップ自体がJSONオブジェクトとしてエンコードされるのではなく、そのマップのキーと値が直接親のJSONオブジェクトにマージされます。

これにより、デコード時に「オーバーフロー」したフィールドを、元のJSON構造を維持したまま再エンコードすることが可能になります。

実装の詳細

  • decode.go: object メソッド内で、JSONオブジェクトのキーを処理するロジックが変更されました。新しい subValue ヘルパー関数が導入され、キーに対応する構造体フィールドを見つけるか、"overflow" フィールドに値を格納するロジックがカプセル化されました。
  • encode.go: structEncoderencode メソッド内で、"overflow" フィールドの処理が追加されました。startOverflowendOverflow というヘルパーメソッドが encodeState に追加され、オーバーフローマップの内容を直接親のJSONオブジェクトに書き込むためのバッファ操作を管理します。
  • fieldByIndex 関数が変更され、ポインタフィールドが nil の場合に create フラグに基づいて新しいインスタンスを生成する機能が追加されました。これは、オーバーフローマップが nil の場合に自動的にマップを初期化するために使用されます。
  • field 構造体に overflow ブール値が追加され、構造体タグのパース時に "overflow" オプションが指定されたかどうかを記録します。

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

src/pkg/encoding/json/decode.go

--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -512,44 +514,7 @@ func (d *decodeState) object(v reflect.Value) {
 		}
 
 		// Figure out field corresponding to key.
-		var subv reflect.Value
-		destring := false // whether the value is wrapped in a string to be decoded first
-
-		if v.Kind() == reflect.Map {
-			elemType := v.Type().Elem()
-			if !mapElem.IsValid() {
-				mapElem = reflect.New(elemType).Elem()
-			} else {
-				mapElem.Set(reflect.Zero(elemType))
-			}
-			subv = mapElem
-		} else {
-			var f *field
-			fields := cachedTypeFields(v.Type())
-			for i := range fields {
-				ff := &fields[i]
-				if ff.name == key {
-					f = ff
-					break
-				}
-				if f == nil && strings.EqualFold(ff.name, key) {
-					f = ff
-				}
-			}
-			if f != nil {
-				subv = v
-				destring = f.quoted
-				for _, i := range f.index {
-					if subv.Kind() == reflect.Ptr {
-						if subv.IsNil() {
-							subv.Set(reflect.New(subv.Type().Elem()))
-						}
-						subv = subv.Elem()
-					}
-					subv = subv.Field(i)
-				}
-			}
-		}
+		subv, mapv, destring := subValue(v, key)
 
 		// Read : before value.
 		if op == scanSkipSpace {
@@ -569,9 +534,9 @@ func (d *decodeState) object(v reflect.Value) {
 
 		// Write value back to map;
 		// if using struct, subv points into struct already.
-		if v.Kind() == reflect.Map {
-			kv := reflect.ValueOf(key).Convert(v.Type().Key())
-			v.SetMapIndex(kv, subv)
+		if mapv.IsValid() {
+			kv := reflect.ValueOf(key).Convert(mapv.Type().Key())
+			mapv.SetMapIndex(kv, subv)
 		}
 
 		// Next token must be , or }.
@@ -585,6 +550,57 @@ func (d *decodeState) object(v reflect.Value) {
 	}
 }
 
+// subValue returns (and allocates, if necessary) the field in the struct or
+// map v whose name matches key.
+func subValue(v reflect.Value, key string) (subv, mapv reflect.Value, destring bool) {
+	// Create new map element.
+	if v.Kind() == reflect.Map {
+		subv = reflect.New(v.Type().Elem()).Elem()
+		mapv = v
+		return
+	}
+
+	// Get struct field.
+	var f *field
+	fields := cachedTypeFields(v.Type())
+	for i := range fields {
+		ff := &fields[i]
+		if ff.name == key {
+			f = ff
+			break
+		}
+		if f == nil && strings.EqualFold(ff.name, key) {
+			f = ff
+		}
+	}
+	if f != nil {
+		subv = fieldByIndex(v, f.index, true)
+		destring = f.quoted
+		return
+	}
+
+	// Decode into overflow field if present.
+	for _, f := range fields {
+		if f.overflow {
+			// Find overflow field.
+			mapv = fieldByIndex(v, f.index, true)
+			if k := mapv.Kind(); k != reflect.Map {
+				panic("unsupported overflow field kind: " + k.String())
+			}
+			// Make map if necessary.
+			if mapv.IsNil() {
+				mapv.Set(reflect.MakeMap(mapv.Type()))
+			}
+			// Create new map element.
+			subv = reflect.New(mapv.Type().Elem()).Elem()
+			return
+		}
+	}
+
+	// Not found.
+	return
+}
+
 // literal consumes a literal from d.data[d.off-1:], decoding into the value v.
 // The first byte of the literal has been read already
 // (that's how the caller knows it's a literal).

src/pkg/encoding/json/encode.go

--- a/src/pkg/encoding/json/encode.go
+++ b/src/pkg/encoding/json/encode.go
@@ -109,6 +109,10 @@ import (
 // an anonymous struct field in both current and earlier versions, give the field
 // a JSON tag of "-".
 //
+// The "overflow" option may be used with a struct field of a map type to
+// indicate that the map contents should be marshalled as if the keys are part
+// of the struct object itself.
+//
 // Map values encode as JSON objects.
 // The map's key type must be string; the object keys are used directly
 // as map keys.
@@ -239,6 +243,32 @@ var hex = "0123456789abcdef"
 type encodeState struct {
 	bytes.Buffer // accumulated output
 	scratch      [64]byte
+	overflow     int
+}
+
+func (e *encodeState) startOverflow() {
+	e.overflow = e.Len()
+}
+
+func (e *encodeState) endOverflow() {
+	if e.overflow == 0 {
+		panic("endOverflow called before startOverflow")
+	}
+	start, end := e.overflow, e.Len()
+	b := e.Bytes()
+	if b[start] == '{' && b[end-1] == '}' {
+		// Remove surrounding { and }.
+		copy(b[start:], b[start+1:])
+		e.Truncate(end - 2)
+	} else if bytes.Equal(b[start:end], []byte("null")) {
+		// Drop "null".
+		e.Truncate(start)
+	}
+	// Remove trailing comma if overflow value was null or {}.
+	if start > 0 && e.Len() == start && b[start-1] == ',' {
+		e.Truncate(start - 1)
+	}
+	e.overflow = 0
 }
 
 // TODO(bradfitz): use a sync.Cache here
@@ -582,7 +612,7 @@ func (se *structEncoder) encode(e *encodeState, v reflect.Value, quoted bool) {
 	e.WriteByte('{')
 	first := true
 	for i, f := range se.fields {
-		fv := fieldByIndex(v, f.index)
+		fv := fieldByIndex(v, f.index, false)
 		if !fv.IsValid() || f.omitEmpty && isEmptyValue(fv) {
 			continue
 		}
@@ -591,6 +621,16 @@ func (se *structEncoder) encode(e *encodeState, v reflect.Value, quoted bool) {
 		} else {
 			e.WriteByte(',')
 		}
+		if f.overflow {
+			if tenc := se.fieldEncs[i]; tenc != nil {
+				e.startOverflow()
+				tenc(e, fv, f.quoted)
+				e.endOverflow()
+			} else {
+				panic("no encoder for " + fv.String())
+			}
+			continue
+		}
 		e.string(f.name)
 		e.WriteByte(':')
 		if tenc := se.fieldEncs[i]; tenc != nil {
@@ -610,7 +650,7 @@ func newStructEncoder(t reflect.Type, vx reflect.Value) encoderFunc {
 	\tfieldEncs: make([]encoderFunc, len(fields)),
 	}
 	for i, f := range fields {
-		vxf := fieldByIndex(vx, f.index)
+		vxf := fieldByIndex(vx, f.index, false)
 		if vxf.IsValid() {
 			se.fieldEncs[i] = typeEncoder(vxf.Type(), vxf)
 		}
@@ -750,11 +790,16 @@ func isValidTag(s string) bool {
 	return true
 }
 
-func fieldByIndex(v reflect.Value, index []int) reflect.Value {
+// fieldByIndex fetches (and allocates, if create is true) the field in v
+// indentified by index.
+func fieldByIndex(v reflect.Value, index []int, create bool) reflect.Value {
 	for _, i := range index {
 		if v.Kind() == reflect.Ptr {
 			if v.IsNil() {
-				return reflect.Value{}
+				if !create {
+					return reflect.Value{}
+				}
+				v.Set(reflect.New(v.Type().Elem()))
 			}
 			v = v.Elem()
 		}
@@ -926,6 +971,7 @@ type field struct {
 	typ       reflect.Type
 	omitEmpty bool
 	quoted    bool
+	overflow  bool
 }
 
 // byName sorts field by name, breaking ties with depth,
@@ -1027,8 +1073,15 @@ func typeFields(t reflect.Type) []field {
 				if name == "" {
 					name = sf.Name
 				}
-				fields = append(fields, field{name, tagged, index, ft,
-					opts.Contains("omitempty"), opts.Contains("string")})
+				fields = append(fields, field{
+					name:      name,
+					tag:       tagged,
+					index:     index,
+					typ:       ft,
+					omitEmpty: opts.Contains("omitempty"),
+					quoted:    opts.Contains("string"),
+					overflow:  opts.Contains("overflow")},
+				)
 				if count[f.typ] > 1 {
 					// If there were multiple instances, add a second,
 					// so that the annihilation code will see a duplicate.

コアとなるコードの解説

decode.go の変更点

  • subValue 関数の導入:
    • 以前は object メソッド内に直接記述されていた、JSONキーに対応する構造体フィールドを見つけるロジックが subValue という新しいヘルパー関数に切り出されました。
    • この関数は、与えられた key に対応する構造体フィールド (subv) と、そのフィールドがマップ型である場合のマップの reflect.Value (mapv)、そして文字列としてデコードする必要があるか (destring) を返します。
    • 特に重要なのは、構造体のフィールドを走査し、overflow タグが設定されたマップ型のフィールドを見つけた場合、そのマップを初期化(reflect.MakeMap)し、そのマップに新しい要素を追加するための subv を準備するロジックが追加されたことです。これにより、未知のJSONフィールドがこのオーバーフローマップに格納される準備が整います。
  • object メソッドの簡素化:
    • object メソッドは subValue を呼び出すことで、フィールドの特定と値の準備のロジックが大幅に簡素化されました。
    • デコードされた値をマップに設定する際も、mapv.IsValid() をチェックし、mapv.SetMapIndex(kv, subv) を呼び出すことで、オーバーフローマップへの書き込みも統一的に処理されます。

encode.go の変更点

  • encodeState への overflow フィールドとヘルパーメソッドの追加:
    • encodeState 構造体に overflow int フィールドが追加されました。これは、オーバーフローマップの内容を直接親のJSONオブジェクトに書き込む際のバッファ操作の開始位置を記録するために使用されます。
    • startOverflow(): 現在のバッファの長さを overflow フィールドに保存します。
    • endOverflow(): startOverflow() で記録された位置から現在のバッファの末尾までの内容を処理します。具体的には、オーバーフローマップがエンコードされた結果が {...} の形式であれば {} を削除し、null であれば null を削除します。また、必要に応じて先行するカンマも削除し、オーバーフローマップの内容が親のJSONオブジェクトにシームレスにマージされるようにします。
  • structEncoderencode メソッドの変更:
    • 構造体のフィールドをエンコードするループ内で、f.overflowtrue の場合(つまり、overflow タグが付与されたマップフィールドの場合)の特別な処理が追加されました。
    • この場合、e.startOverflow() を呼び出してバッファの開始位置を記録し、オーバーフローマップをエンコードし、e.endOverflow() を呼び出して余分な {}null を削除します。これにより、マップの内容が直接親のJSONオブジェクトに展開されます。

fieldByIndex 関数の変更

  • fieldByIndex 関数に create bool 引数が追加されました。
  • この createtrue で、かつポインタ型のフィールドが nil の場合、reflect.New(v.Type().Elem()) を使って新しいインスタンスが作成され、そのポインタが設定されるようになりました。これは、デコード時にオーバーフローマップがまだ初期化されていない場合に、自動的にマップを作成するために利用されます。

field 構造体の変更

  • field 構造体に overflow bool フィールドが追加されました。これは、構造体タグをパースする際に "overflow" オプションが指定されたかどうかを記録するために使用されます。

これらの変更により、encoding/json パッケージは、Goの構造体とJSONデータ間のマッピングにおいて、より柔軟で堅牢な「オーバーフロー」処理をサポートするようになりました。

関連リンク

参考にした情報源リンク

  • コミットメッセージと変更されたソースコード (src/pkg/encoding/json/decode.go, src/pkg/encoding/json/encode.go, src/pkg/encoding/json/decode_test.go, src/pkg/encoding/json/encode_test.go, src/pkg/encoding/json/example_test.go)
  • Go言語の encoding/json パッケージのドキュメント (一般的なJSON処理の理解のため)
  • Go言語の reflect パッケージのドキュメント (リフレクションの理解のため)