[インデックス 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の設計思想について言及している可能性があります。