[インデックス 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
は以下の挙動を示します。
- 入力JSONオブジェクトのキーが、Goの構造体の他のフィールド名(JSONタグで指定された名前を含む)と一致するかどうかをチェックします。
- 一致するフィールドがあれば、通常通りそのフィールドに値をデコードします。
- 一致しないキーと値のペアは、
"overflow"
タグが付与されたマップ型のフィールドに格納されます。
これにより、JSONデータに予期せぬフィールドが含まれていても、それらの情報が失われることなく、特定のマップに集約されるようになります。
マーシャリング (エンコード) 時の挙動
"overflow"
タグが付与されたマップ型のフィールドが存在する場合、json.Marshal
は以下の挙動を示します。
- 通常通り、構造体の各フィールドをJSONオブジェクトのキーと値に変換します。
"overflow"
タグが付与されたマップ型のフィールドの内容は、そのマップ自体がJSONオブジェクトとしてエンコードされるのではなく、そのマップのキーと値が直接親のJSONオブジェクトにマージされます。
これにより、デコード時に「オーバーフロー」したフィールドを、元のJSON構造を維持したまま再エンコードすることが可能になります。
実装の詳細
decode.go
:object
メソッド内で、JSONオブジェクトのキーを処理するロジックが変更されました。新しいsubValue
ヘルパー関数が導入され、キーに対応する構造体フィールドを見つけるか、"overflow"
フィールドに値を格納するロジックがカプセル化されました。encode.go
:structEncoder
のencode
メソッド内で、"overflow"
フィールドの処理が追加されました。startOverflow
とendOverflow
というヘルパーメソッドが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オブジェクトにシームレスにマージされるようにします。
structEncoder
のencode
メソッドの変更:- 構造体のフィールドをエンコードするループ内で、
f.overflow
がtrue
の場合(つまり、overflow
タグが付与されたマップフィールドの場合)の特別な処理が追加されました。 - この場合、
e.startOverflow()
を呼び出してバッファの開始位置を記録し、オーバーフローマップをエンコードし、e.endOverflow()
を呼び出して余分な{}
やnull
を削除します。これにより、マップの内容が直接親のJSONオブジェクトに展開されます。
- 構造体のフィールドをエンコードするループ内で、
fieldByIndex
関数の変更
fieldByIndex
関数にcreate bool
引数が追加されました。- この
create
がtrue
で、かつポインタ型のフィールドがnil
の場合、reflect.New(v.Type().Elem())
を使って新しいインスタンスが作成され、そのポインタが設定されるようになりました。これは、デコード時にオーバーフローマップがまだ初期化されていない場合に、自動的にマップを作成するために利用されます。
field
構造体の変更
field
構造体にoverflow bool
フィールドが追加されました。これは、構造体タグをパースする際に"overflow"
オプションが指定されたかどうかを記録するために使用されます。
これらの変更により、encoding/json
パッケージは、Goの構造体とJSONデータ間のマッピングにおいて、より柔軟で堅牢な「オーバーフロー」処理をサポートするようになりました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/466001d05d366cbc97edfb65dc6f5cb883df0498
- Go CL (Code Review): https://golang.org/cl/13180043
参考にした情報源リンク
- コミットメッセージと変更されたソースコード (
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
パッケージのドキュメント (リフレクションの理解のため)