[インデックス 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)