[インデックス 10155] ファイルの概要
このコミットは、Go言語の標準ライブラリである encoding/json
パッケージにおける、nil
スライスのJSONマーシャリング(Goのデータ構造からJSONへの変換)とアンマーシャリング(JSONからGoのデータ構造への変換)の挙動を修正するものです。具体的には、nil
スライスがJSONでは null
として表現され、JSONの null
がGoの nil
スライスとして正しくパースされるように変更されました。これにより、Goの nil
スライスと空のスライス []
のJSON表現が区別され、より直感的で期待される挙動が実現されました。
コミット
- コミットハッシュ:
48c75c5f9c8e97b87fbd8f24dffa73d6b2148691
- Author: Alexander Reece awreece@gmail.com
- Date: Mon Oct 31 13:59:23 2011 -0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/48c75c5f9c8e97b87fbd8f24dffa73d6b2148691
元コミット内容
json: Properly handle nil slices.
Marshal nil slices as null and parse null value as a nil slice.
Fixes #2278.
R=rsc
CC=golang-dev
https://golang.org/cl/5257053
変更の背景
このコミットは、Goの encoding/json
パッケージが nil
スライスをJSONにマーシャリングする際に、空のJSON配列 []
として扱ってしまうという問題(Issue #2278)を修正するために行われました。また、JSONの null
値をGoの nil
スライスとして正しくアンマーシャリングすることも目的としています。
Goにおいて、スライスは nil
である状態と、要素を持たない空の状態 []
が区別されます。しかし、この修正以前は、json.Marshal
は nil
スライスを null
ではなく []
として出力していました。これは、JSONの仕様や他の言語の挙動と異なる場合があり、特にデータベースのNULL許容フィールドなどと連携する際に問題となる可能性がありました。
例えば、Goの構造体で []string
型のフィールドが nil
の場合、JSON出力が []
となると、受信側が「空のリスト」と解釈してしまい、「値が存在しない(null)」という意図が伝わらないという不整合が生じていました。この修正により、Goの nil
スライスはJSONの null
に、JSONの null
はGoの nil
スライスに、それぞれ正しくマッピングされるようになります。
前提知識の解説
Goにおけるスライスとnil
Go言語において、スライスは配列のセグメントを記述するデータ構造です。スライスは内部的にポインタ、長さ、容量の3つの要素を持ちます。
nil
スライス: スライスのゼロ値はnil
です。var s []int
のように宣言されたスライスはnil
であり、そのポインタはnil
、長さは0、容量も0です。nil
スライスは有効なスライスであり、長さが0であるため、多くの操作(len(s)
、cap(s)
、for range s
など)で空のスライスと同様に振る舞います。- 空のスライス:
make([]int, 0)
や[]int{}
のように初期化されたスライスは空のスライスです。これらのスライスはnil
ではありませんが、長さは0です。ポインタは有効なメモリを指しますが、そのメモリには要素がありません。
JSONマーシャリングの文脈では、nil
スライスと空のスライスがJSONでどのように表現されるべきかという点が重要になります。一般的に、JSONでは「値が存在しない」ことを null
で表現し、「空のリスト」を []
で表現します。この区別は、APIの設計やデータの一貫性において非常に重要です。
JSONマーシャリングとアンマーシャリング
- マーシャリング (Marshaling): Goのデータ構造(構造体、スライス、マップなど)をJSON形式のバイト列に変換するプロセスです。
json.Marshal()
関数がこれを行います。 - アンマーシャリング (Unmarshaling): JSON形式のバイト列をGoのデータ構造に変換するプロセスです。
json.Unmarshal()
関数がこれを行います。
encoding/json
パッケージは、Goのデータ型とJSONのデータ型の間で自動的な変換を行います。例えば、Goの string
はJSONの文字列に、Goの int
はJSONの数値に、Goの map
や構造体はJSONのオブジェクトに、Goのスライスや配列はJSONの配列に変換されます。
このコミットの焦点は、Goの nil
スライスがJSONの null
に、そしてJSONの null
がGoの nil
スライスに、それぞれ正しくマッピングされるようにすることです。
技術的詳細
このコミットでは、src/pkg/json/decode.go
と src/pkg/json/encode.go
の2つの主要なファイルが変更され、対応するテストファイル src/pkg/json/decode_test.go
と src/pkg/json/encode_test.go
も更新されています。
json.Marshal
の変更 (encode.go
)
以前の json.Marshal
は、reflect.Slice
型の値を処理する際に、nil
であるかどうかを区別せず、常に空のJSON配列 []
として出力していました。
このコミットでは、encode.go
の reflect.Slice
を処理する case
に v.IsNil()
のチェックが追加されました。
// src/pkg/json/encode.go
case reflect.Slice:
if v.IsNil() {
e.WriteString("null")
break
}
// Slices can be marshalled as nil, but otherwise are handled
// as arrays.
fallthrough
case reflect.Array:
// ... (既存の配列処理ロジック)
この変更により、reflect.Slice
の値が nil
である場合、e.WriteString("null")
が呼び出され、JSON出力が "null"
となります。nil
でない場合は fallthrough
を使用して reflect.Array
の処理ロジックにフォールスルーし、通常の配列としてマーシャリングされます。これにより、Goの nil
スライスはJSONの null
として正しく表現されるようになりました。
json.Unmarshal
の変更 (decode.go
)
json.Unmarshal
は、JSONの null
値をGoのデータ構造にデコードする際に、reflect.Slice
型のフィールドに対して nil
を設定できるように変更されました。
// src/pkg/json/decode.go
func (d *decodeState) literalStore(item []byte, v reflect.Value) {
// ...
case 'n': // null
// ...
switch v.Kind() {
default:
d.saveError(&UnmarshalTypeError{"null", v.Type()})
case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: // ここに reflect.Slice が追加
v.Set(reflect.Zero(v.Type()))
}
// ...
}
literalStore
関数は、JSONの null
リテラルをGoの対応する型にデコードする役割を担っています。以前は reflect.Slice
が null
を受け入れる型として明示的にリストされていませんでした。この変更により、reflect.Slice
が case reflect.Interface, reflect.Ptr, reflect.Map
と同じように null
を受け入れ、v.Set(reflect.Zero(v.Type()))
を呼び出すことで、そのスライスを nil
に設定できるようになりました。reflect.Zero(v.Type())
は、指定された型のゼロ値を返します。スライス型の場合、そのゼロ値は nil
スライスです。
テストの変更 (decode_test.go
, encode_test.go
)
テストファイルでは、nil
スライスが null
としてマーシャリングされ、null
が nil
スライスとしてアンマーシャリングされることを検証するためのテストケースが追加・修正されています。
decode_test.go
では、allValueIndent
および pallValueIndent
変数内の NilSlice
や EmptySlice
の期待されるJSON表現が []
から null
に変更されています。これにより、JSONの null
がGoの nil
スライスとして正しくデコードされることを確認します。
encode_test.go
では、optionalsExpected
変数内の slr
(slice) の期待されるJSON表現が []
から null
に変更されています。これにより、Goの nil
スライスがJSONの null
として正しくエンコードされることを確認します。
これらの変更により、Goの encoding/json
パッケージは、nil
スライスと空のスライスのJSON表現をより厳密に区別し、JSONの null
とGoの nil
スライスの間のマッピングを正確に行うようになりました。
コアとなるコードの変更箇所
src/pkg/json/decode.go
--- a/src/pkg/json/decode.go
+++ b/src/pkg/json/decode.go
@@ -588,7 +588,7 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value) {
switch v.Kind() {
default:
d.saveError(&UnmarshalTypeError{"null", v.Type()})
- case reflect.Interface, reflect.Ptr, reflect.Map:
+ case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice:
v.Set(reflect.Zero(v.Type()))
}
src/pkg/json/decode_test.go
--- a/src/pkg/json/decode_test.go
+++ b/src/pkg/json/decode_test.go
@@ -456,7 +456,7 @@ var allValueIndent = `{\
"PSlice": null,
"PSliceP": null,
"EmptySlice": [],
- "NilSlice": [],
+ "NilSlice": null,
"StringSlice": [
"str24",
"str25",
@@ -528,8 +528,8 @@ var pallValueIndent = `{\
},
"EmptyMap": null,
"NilMap": null,
- "Slice": [],
- "SliceP": [],
+ "Slice": null,
+ "SliceP": null,
"PSlice": [
{
"Tag": "tag20"
@@ -547,10 +547,10 @@ var pallValueIndent = `{\
"Tag": "tag23"
}
],
- "EmptySlice": [],
- "NilSlice": [],
- "StringSlice": [],
- "ByteSlice": "",
+ "EmptySlice": null,
+ "NilSlice": null,
+ "StringSlice": null,
+ "ByteSlice": null,
"Small": {
"Tag": ""
},
src/pkg/json/encode.go
--- a/src/pkg/json/encode.go
+++ b/src/pkg/json/encode.go
@@ -352,7 +352,15 @@ func (e *encodeState) reflectValueQuoted(v reflect.Value, quoted bool) {
}
e.WriteByte('}')
- case reflect.Array, reflect.Slice:
+ case reflect.Slice:
+ if v.IsNil() {
+ e.WriteString("null")
+ break
+ }
+ // Slices can be marshalled as nil, but otherwise are handled
+ // as arrays.
+ fallthrough
+ case reflect.Array:
if v.Type() == byteSliceType {
e.WriteByte('"')
s := v.Interface().([]byte)
src/pkg/json/encode_test.go
--- a/src/pkg/json/encode_test.go
+++ b/src/pkg/json/encode_test.go
@@ -28,7 +28,7 @@ type Optionals struct {
var optionalsExpected = `{\
"sr": "",
"omitempty": 0,\
- "slr": [],
+ "slr": null,\
"mr": {}\
}`
コアとなるコードの解説
src/pkg/json/decode.go
の変更
literalStore
関数は、JSONの null
リテラルをGoの対応する型にデコードする際に呼び出されます。
変更前は、reflect.Interface
, reflect.Ptr
, reflect.Map
のみが null
を受け入れて reflect.Zero(v.Type())
でゼロ値(nil
)に設定されていました。
変更後、reflect.Slice
がこのリストに追加されました。これにより、JSONの null
がGoのスライス型にデコードされる際に、そのスライスが nil
に設定されるようになりました。これは、nil
スライスがGoにおけるスライスのゼロ値であるため、自然な挙動です。
src/pkg/json/encode.go
の変更
reflectValueQuoted
関数は、Goの reflect.Value
をJSONにエンコードする主要なロジックを含んでいます。
変更前は、reflect.Array
と reflect.Slice
が同じ case
で処理されており、nil
スライスと空のスライスが区別されずに、常に空のJSON配列 []
としてエンコードされていました。
変更後、reflect.Slice
が独立した case
として分離されました。この新しい reflect.Slice
の case
内で、v.IsNil()
を使ってスライスが nil
であるかどうかがチェックされます。
- もし
v.IsNil()
がtrue
であれば、e.WriteString("null")
を呼び出してJSON出力に"null"
を書き込み、break
で処理を終了します。 - もし
v.IsNil()
がfalse
であれば、fallthrough
を使用してreflect.Array
のcase
に処理を移します。これにより、nil
でないスライス(空のスライス[]
や要素を持つスライス)は、既存の配列エンコードロジックによってJSON配列としてエンコードされます。
この変更により、Goの nil
スライスはJSONの null
として、Goの空のスライス []
はJSONの []
として、それぞれ明確に区別されてエンコードされるようになりました。
テストファイルの変更 (decode_test.go
, encode_test.go
)
テストファイルの変更は、上記のコード変更が意図した通りに動作することを確認するためのものです。
decode_test.go
では、JSON文字列内のNilSlice
やEmptySlice
の期待値が[]
からnull
に変更されています。これは、JSONのnull
がGoのnil
スライスとして正しくアンマーシャリングされることを検証します。encode_test.go
では、Goの構造体をJSONにマーシャリングした際の期待値として、nil
スライスに対応するJSONがnull
となるように修正されています。
これらのテストの修正は、新しい nil
スライスの処理ロジックが正しく実装され、既存の動作に悪影響を与えていないことを保証します。
関連リンク
- Go Issue #2278: json: Marshal nil slices as null, parse null as nil slice
- Go CL 5257053: https://golang.org/cl/5257053
参考にした情報源リンク
- Go言語の公式ドキュメント (
encoding/json
パッケージ) - Go言語のスライスに関する公式ドキュメント
- JSONの仕様 (RFC 7159)
- Go言語のIssueトラッカー (GitHub)
- Go言語のコードレビューシステム (Gerrit/CL)
参考にした情報源リンク
- Go言語の公式ドキュメント (
encoding/json
パッケージ) - Go言語のスライスに関する公式ドキュメント
- JSONの仕様 (RFC 7159)
- Go言語のIssueトラッカー (GitHub)
- Go言語のコードレビューシステム (Gerrit/CL)
- Go issue 2278, titled "json: marshal nil slice as
null
" (Web検索結果より) - dev.to - Go json.Marshal nil slice as null (Web検索結果より)
- medium.com - Go: Marshal nil slices as null (Web検索結果より)
- reddit.com - Go json.Marshal nil slice as null (Web検索結果より)
- stackoverflow.com - Go json.Marshal nil slice as null (Web検索結果より)
- ycombinator.com - Go json.Marshal nil slice as null (Web検索結果より)
注記: 上記のWeb検索結果は、このコミットが修正したIssue #2278に関する一般的な議論や、Goの encoding/json
パッケージにおける nil
スライスの挙動に関する情報を提供しています。このコミット自体は2011年のものであり、Web検索結果の一部はそれ以降の議論や、nil
スライスと空のスライスの区別に関するGoの設計思想について言及している可能性があります。