[インデックス 14764] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json
パッケージにおける、JSONのデコード処理の改善に関するものです。具体的には、Goのマップ型 map[K]V
をJSONにエンコード・デコードする際に、キーの型 K
が string
型だけでなく、その基底型が string
であるカスタム型(type MyString string
のような型)も適切に扱えるように修正されています。これにより、より柔軟なJSONのシリアライズ・デシリアライズが可能になりました。
コミット
commit a4600126d9e4fcbd8e9ea3072eff7ea5822f2014
Author: Ryan Slade <ryanslade@gmail.com>
Date: Sun Dec 30 15:40:42 2012 +1100
encoding/json: encode map key is of string kind, decode only of string type
Allows encoding and decoding of maps with key of string kind, not just string type.
Fixes #3519.
R=rsc, dave
CC=golang-dev
https://golang.org/cl/6943047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a4600126d9e4fcbd8e9ea3072eff7ea5822f2014
元コミット内容
encoding/json: encode map key is of string kind, decode only of string type
Allows encoding and decoding of maps with key of string kind, not just string type.
Fixes #3519.
R=rsc, dave
CC=golang-dev
https://golang.org/cl/6943047
変更の背景
Goの encoding/json
パッケージは、Goのデータ構造とJSONデータの間で変換を行うための重要なライブラリです。以前のバージョンでは、map[K]V
のようなマップ型をJSONにデコードする際、キーの型 K
が厳密に組み込みの string
型である必要がありました。
しかし、Goでは type MyString string
のように、既存の型を基底型として新しい型を定義することができます。このようなカスタム型は、基底型と同じ「種類 (Kind)」を持ちますが、異なる「型 (Type)」として扱われます。例えば、MyString
は string
とは異なる型ですが、その基底となる種類は string
です。
このコミット以前は、map[MyString]int
のようなマップをJSONからデコードしようとすると、encoding/json
はキーの型が string
ではないと判断し、エラーを発生させていました。これは、ユーザーがカスタム型をマップのキーとして使用したい場合に不便であり、Goの型システムの柔軟性を十分に活用できていないという問題がありました。
この変更は、この制限を緩和し、マップのキーが string
型そのものでなくても、その基底となる種類が string
であればデコードを許可することで、より実用的なJSON処理を可能にすることを目的としています。コミットメッセージにある Fixes #3519
は、この問題が報告されていたことを示唆していますが、現在のGoのGitHubリポジトリでは該当するIssueは見つかりませんでした。しかし、この変更が特定の不具合や要望に対応するものであることは明らかです。
前提知識の解説
Goの reflect
パッケージにおける Kind
と Type
Go言語の reflect
パッケージは、実行時に型情報を検査・操作するための機能を提供します。このパッケージを理解する上で重要な概念が「Kind (種類)」と「Type (型)」です。
-
Type (型):
reflect.Type
は、Goプログラムにおける具体的な型を表します。例えば、int
、string
、[]byte
、struct { Name string; Age int }
、type MyString string
など、それぞれが異なるreflect.Type
を持ちます。type MyString string
と定義されたMyString
は、組み込みのstring
とは異なるreflect.Type
です。 -
Kind (種類):
reflect.Kind
は、Goの型が属する基本的なカテゴリを表します。これは、reflect.Int
、reflect.String
、reflect.Slice
、reflect.Struct
、reflect.Map
など、Goが持つプリミティブな型の種類に対応します。type MyString string
のMyString
型は、その基底となる種類がstring
であるため、reflect.String
のKind
を持ちます。
encoding/json
パッケージは、Goのデータ構造とJSONの間で変換を行う際に、この reflect
パッケージを内部的に利用しています。特に、JSONオブジェクトのキーは常に文字列であるため、GoのマップをJSONオブジェクトに変換する際、マップのキーが文字列型であるかどうかのチェックが必要になります。
encoding/json
パッケージのマップ処理
encoding/json
パッケージがGoのマップを処理する際、JSONオブジェクトのキーは文字列であるという性質上、Goのマップのキーも文字列に変換可能である必要があります。これまでは、この変換可能性のチェックが厳密に reflect.TypeOf("")
(つまり組み込みの string
型) との一致を求めていました。
このコミットは、このチェックを reflect.Kind
に変更することで、string
型を基底とするカスタム型もマップのキーとして許容するように拡張しています。
技術的詳細
このコミットの技術的な核心は、encoding/json
パッケージ内の decode.go
ファイルにおける、マップのキー型チェックのロジック変更と、デコード時のキーの型変換処理の追加です。
変更点1: キー型の Kind
チェックへの変更
以前のコードでは、JSONオブジェクトをGoのマップにデコードする際、マップのキーの型が reflect.TypeOf("")
、つまり組み込みの string
型と完全に一致するかどうかをチェックしていました。
// Old code
if t.Key() != reflect.TypeOf("") {
d.saveError(&UnmarshalTypeError{"object", v.Type()})
break
}
このコミットでは、このチェックを reflect.Kind
に変更しました。
// New code
if t.Key().Kind() != reflect.String {
d.saveError(&UnmarshalTypeError{"object", v.Type()})
break
}
これにより、マップのキーの reflect.Type
が string
型そのものでなくても、その reflect.Kind
が reflect.String
であれば、デコード処理を続行できるようになりました。例えば、type MyString string
と定義された MyString
型は、reflect.String
の Kind
を持つため、map[MyString]int
のようなマップも正しくデコードの対象となります。
変更点2: デコード時のキーの型変換
JSONから読み取られたキーは常に文字列として扱われます。この文字列をGoのマップの適切なキー型に設定するためには、型変換が必要になります。以前のコードでは、キーが string
型であると仮定して直接 reflect.ValueOf(key)
を使用していました。
// Old code
v.SetMapIndex(reflect.ValueOf(key), subv)
新しいコードでは、reflect.ValueOf(key)
で得られた string
の reflect.Value
を、マップの実際のキー型 (v.Type().Key()
) に変換する Convert
メソッドを導入しました。
// New code
kv := reflect.ValueOf(key).Convert(v.Type().Key())
v.SetMapIndex(kv, subv)
reflect.Value.Convert(Type)
メソッドは、reflect.Value
が保持する値を指定された reflect.Type
に変換します。この場合、JSONから読み取られた文字列 (key
) を、マップのキーとして定義されているカスタム文字列型(例: MyString
)に変換します。これにより、map[MyString]int
のようなマップに対しても、JSONの文字列キーを MyString
型の値としてマップに設定できるようになります。
テストケースの追加
decode_test.go
に TestStringKind
という新しいテスト関数が追加されました。このテストは、type stringKind string
というカスタム文字列型を定義し、map[stringKind]int
型のマップがJSONのエンコード・デコードを正しく行えることを検証しています。
このテストは以下の手順で実行されます。
stringKind
型をキーとするマップm1
を初期化します。m1
をjson.Marshal
でJSONバイト列にエンコードします。- エンコードされたJSONバイト列を
json.Unmarshal
で別のmap[stringKind]int
型のマップm2
にデコードします。 m1
とm2
がreflect.DeepEqual
で等しいことを確認します。
このテストの成功は、上記の decode.go
の変更が意図通りに機能し、カスタム文字列型をキーとするマップのJSONエンコード・デコードが正しく行われることを保証します。
コアとなるコードの変更箇所
src/pkg/encoding/json/decode.go
の変更点:
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -430,9 +430,9 @@ func (d *decodeState) object(v reflect.Value) {
// Check type of target: struct or map[string]T
switch v.Kind() {
case reflect.Map:
- // map must have string type
+ // map must have string kind
t := v.Type()
- if t.Key() != reflect.TypeOf("") {
+ if t.Key().Kind() != reflect.String {
d.saveError(&UnmarshalTypeError{"object", v.Type()})
break
}
@@ -536,10 +536,12 @@ func (d *decodeState) object(v reflect.Value) {
} else {
d.value(subv)
}
+\
// Write value back to map;
// if using struct, subv points into struct already.
if v.Kind() == reflect.Map {
- v.SetMapIndex(reflect.ValueOf(key), subv)
+ kv := reflect.ValueOf(key).Convert(v.Type().Key())
+ v.SetMapIndex(kv, subv)
}
// Next token must be , or }.
src/pkg/encoding/json/decode_test.go
の変更点:
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -1000,3 +1000,28 @@ func TestUnmarshalNulls(t *testing.T) {\n \t\tt.Errorf(\"Unmarshal of null values affected primitives\")\n \t}\n }\n+\n+func TestStringKind(t *testing.T) {\n+\ttype stringKind string\n+\ttype aMap map[stringKind]int\n+\n+\tvar m1, m2 map[stringKind]int\n+\tm1 = map[stringKind]int{\n+\t\t\"foo\": 42,\n+\t}\n+\n+\tdata, err := Marshal(m1)\n+\tif err != nil {\n+\t\tt.Errorf(\"Unexpected error marshalling: %v\", err)\n+\t}\n+\n+\terr = Unmarshal(data, &m2)\n+\tif err != nil {\n+\t\tt.Errorf(\"Unexpected error unmarshalling: %v\", err)\n+\t}\n+\n+\tif !reflect.DeepEqual(m1, m2) {\n+\t\tt.Error(\"Items should be equal after encoding and then decoding\")\n+\t}\n+\n+}\n```
## コアとなるコードの解説
### `src/pkg/encoding/json/decode.go`
* **`if t.Key().Kind() != reflect.String`**:
* 変更前は `t.Key() != reflect.TypeOf("")` でした。これは、マップのキーの `reflect.Type` が、組み込みの `string` 型の `reflect.Type` と完全に一致するかどうかをチェックしていました。
* 変更後は `t.Key().Kind() != reflect.String` となっています。これは、マップのキーの `reflect.Type` が持つ `Kind` が `reflect.String` であるかどうかをチェックします。これにより、`type MyString string` のような、基底型が `string` であるカスタム型もマップのキーとして許容されるようになります。
* **`kv := reflect.ValueOf(key).Convert(v.Type().Key())`**:
* 変更前は `v.SetMapIndex(reflect.ValueOf(key), subv)` でした。これは、JSONから読み取られた文字列 `key` を直接 `reflect.ValueOf` で `reflect.Value` に変換し、マップのインデックスとして設定していました。これはキーが `string` 型である場合にのみ正しく機能します。
* 変更後は、`reflect.ValueOf(key)` で得られた `string` の `reflect.Value` を、`Convert` メソッドを使ってマップの実際のキー型 (`v.Type().Key()`) に変換しています。例えば、マップのキー型が `MyString` であれば、`key` (文字列) は `MyString` 型の `reflect.Value` に変換され、それがマップのインデックスとして設定されます。これにより、カスタム文字列型をキーとするマップへのデコードが可能になります。
### `src/pkg/encoding/json/decode_test.go`
* **`TestStringKind` 関数**:
* この新しいテスト関数は、`encoding/json` パッケージがカスタム文字列型をキーとするマップを正しく処理できることを検証します。
* `type stringKind string` でカスタム型を定義し、`map[stringKind]int` 型のマップ `m1` を作成します。
* `json.Marshal` と `json.Unmarshal` を使用して、`m1` をJSONにエンコードし、その後 `m2` にデコードします。
* 最後に `reflect.DeepEqual(m1, m2)` を使って、エンコード・デコード前後でマップの内容が完全に一致するかを確認します。これにより、このコミットによる変更が正しく機能していることが保証されます。
## 関連リンク
* Go CL 6943047: [https://golang.org/cl/6943047](https://golang.org/cl/6943047)
* Go Issue #3519: GitHubのgolang/goリポジトリでは該当するIssueは見つかりませんでした。
## 参考にした情報源リンク
* Go言語 `reflect` パッケージのドキュメント: [https://pkg.go.dev/reflect](https://pkg.go.dev/reflect)
* Go言語 `encoding/json` パッケージのドキュメント: [https://pkg.go.dev/encoding/json](https://pkg.go.dev/encoding/json)