[インデックス 14381] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json
パッケージにおけるJSONデコードの挙動を修正するものです。具体的には、JSONデータ内の null
値がGoのプリミティブ型や文字列型にデコードされる際に、エラーを発生させずにスキップする(無視する)ように変更されています。これにより、JSONの柔軟な null
の扱いに対応し、より堅牢なデコード処理を実現しています。
コミット
commit c90739e41ebb7e0c0adc1bbdad61a08dc240dffe
Author: Rick Arnold <rickarnoldjr@gmail.com>
Date: Mon Nov 12 15:35:11 2012 -0500
encoding/json: skip unexpected null values
As discussed in issue 2540, nulls are allowed for any type in JSON so they should not result in an error during Unmarshal.
Fixes #2540.
R=rsc
CC=golang-dev
https://golang.org/cl/6759043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c90739e41ebb7e0c0adc1bbdad61a08dc240dffe
元コミット内容
encoding/json: skip unexpected null values
As discussed in issue 2540, nulls are allowed for any type in JSON so they should not result in an error during Unmarshal.
Fixes #2540.
R=rsc
CC=golang-dev
https://golang.org/cl/6759043
変更の背景
この変更は、Goの encoding/json
パッケージがJSONの null
値を処理する方法に関する既存の問題(Issue 2540)に対応するために行われました。以前の挙動では、JSONの null
値がGoのプリミティブ型(整数、浮動小数点数、ブーリアンなど)や文字列型にデコードされようとすると、UnmarshalTypeError
が発生し、エラーとなっていました。
JSONの仕様では、null
はあらゆるデータ型に対して有効な値として扱われます。例えば、数値が期待されるフィールドに null
が含まれていても、それはJSONとしては有効な表現です。しかし、Goの json.Unmarshal
は、このような場合に厳密な型チェックを行い、Goの型とJSONの null
の間に直接的なマッピングがないためにエラーを返していました。
この厳密な挙動は、特に外部システムから受け取るJSONデータが、Goの構造体の型定義と完全に一致しない場合に問題を引き起こしました。例えば、オプションのフィールドがJSONでは null
で表現されることがよくありますが、Goの構造体でそのフィールドがプリミティブ型として定義されていると、デコードに失敗してしまいます。
このコミットは、このような状況に対応し、JSONの null
値がGoのプリミティブ型や文字列型にデコードされる際に、エラーを発生させずにその値をスキップする(Goの対応するフィールドの値を変更しない)ようにすることで、より寛容で実用的なデコード処理を実現することを目的としています。
前提知識の解説
JSON (JavaScript Object Notation)
JSONは、人間が読み書きしやすく、機械が解析しやすいデータ交換フォーマットです。JavaScriptのオブジェクトリテラルをベースにしていますが、言語に依存しないデータフォーマットとして広く利用されています。
JSONのデータ型には以下のものがあります。
- オブジェクト (object): キーと値のペアの順序なしの集合。キーは文字列で、値はJSONの任意のデータ型。
- 配列 (array): 値の順序付きリスト。値はJSONの任意のデータ型。
- 文字列 (string): Unicode文字のシーケンス。
- 数値 (number): 整数または浮動小数点数。
- ブーリアン (boolean):
true
またはfalse
。 - null: 空の値。
このコミットで特に重要なのは null
の扱いです。JSONの仕様では、null
はあらゆるデータ型が期待される場所に出現し得ます。例えば、{"age": null}
は、age
が数値型であると期待される場合でも有効なJSONです。
Go言語の encoding/json
パッケージ
Go言語の標準ライブラリ encoding/json
は、Goの構造体とJSONデータの間のエンコード(Marshal)およびデコード(Unmarshal)機能を提供します。
json.Marshal
: Goのデータ構造をJSONバイト列に変換します。json.Unmarshal
: JSONバイト列をGoのデータ構造に変換します。
json.Unmarshal
は、JSONデータとGoの構造体のフィールドを対応付け、JSONの値をGoの対応する型のフィールドに変換します。この際、型が一致しない場合や、JSONのデータがGoの型に変換できない場合にエラーが発生することがあります。
reflect
パッケージ
Goの reflect
パッケージは、実行時にプログラムの構造を検査・操作するための機能を提供します。encoding/json
パッケージは、reflect
を利用してGoの構造体のフィールドの型情報を取得し、JSONデータを適切なGoの型にデコードします。
reflect.Value
: Goの値の実行時表現。reflect.Kind()
:reflect.Value
が表す値の基本的な種類(例:reflect.Int
,reflect.String
,reflect.Struct
,reflect.Interface
,reflect.Ptr
,reflect.Map
,reflect.Slice
など)を返します。reflect.Zero(Type)
: 指定された型のゼロ値を返します。
UnmarshalTypeError
encoding/json
パッケージがJSONの値をGoの型にデコードできない場合に発生するエラー型です。例えば、JSONの文字列をGoの整数型にデコードしようとした場合などに発生します。
技術的詳細
このコミットの技術的な核心は、encoding/json
パッケージ内の decode.go
ファイルにある literalStore
メソッドの変更です。このメソッドは、JSONのプリミティブなリテラル値(null
, true
, false
, 数値、文字列)をGoの対応する reflect.Value
に格納する役割を担っています。
変更前は、null
値が検出された際に、Goの reflect.Value
の Kind()
が reflect.Interface
, reflect.Ptr
, reflect.Map
, reflect.Slice
のいずれかである場合にのみ、その値がゼロ値に設定されていました。それ以外の型(default
ケース)では、UnmarshalTypeError
が発生していました。これは、プリミティブ型や文字列型に null
をデコードしようとするとエラーになることを意味します。
変更後は、null
値が検出された際の switch v.Kind()
文から default
ケースが削除されました。これにより、reflect.Interface
, reflect.Ptr
, reflect.Map
, reflect.Slice
以外の型(つまり、プリミティブ型や文字列型)に対して null
が与えられた場合、v.Set(reflect.Zero(v.Type()))
の行が実行されなくなります。代わりに、コメント // otherwise, ignore null for primitives/string
が示唆するように、これらの型に対する null
値は単に無視され、Goの対応するフィールドの値は変更されません。
この変更により、JSONの null
がGoのプリミティブ型や文字列型にマッピングされる際にエラーが発生しなくなり、Goの構造体の既存の値が保持されることになります。これは、JSONの null
が「値がない」ことを意味し、Goのゼロ値に設定するのではなく、既存の値をそのままにしておく方が、多くのユースケースで望ましいという判断に基づいています。
また、decode_test.go
に追加された TestUnmarshalNulls
テストケースは、この新しい挙動を検証しています。このテストでは、Goの All
構造体のすべてのプリミティブ型および文字列型のフィールドに初期値を設定し、それらのフィールドに対応するJSONデータに null
を含むJSON文字列を Unmarshal
しています。テストは、Unmarshal
後もGoの構造体のフィールドの値が初期値のままであり、null
によって上書きされていないことを確認しています。これにより、null
値がプリミティブ型や文字列型に対して正しくスキップされていることが保証されます。
コアとなるコードの変更箇所
src/pkg/encoding/json/decode.go
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -617,12 +617,10 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool
switch c := item[0]; c {
case 'n': // null
switch v.Kind() {
-\t\tdefault:
-\t\t\td.saveError(&UnmarshalTypeError{"null", v.Type()})\
case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice:
v.Set(reflect.Zero(v.Type()))
+\t\t\t// otherwise, ignore null for primitives/string
}
-\
case 't', 'f': // true, false
value := c == 't'
switch v.Kind() {
src/pkg/encoding/json/decode_test.go
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -953,3 +953,50 @@ func TestInterfaceSet(t *testing.T) {
}
}\
}\
+\
+// JSON null values should be ignored for primitives and string values instead of resulting in an error.\
+// Issue 2540\
+func TestUnmarshalNulls(t *testing.T) {\
+ jsonData := []byte(`{\
+\t\t\"Bool\" : null, \
+\t\t\"Int\" : null, \
+\t\t\"Int8\" : null, \
+\t\t\"Int16\" : null, \
+\t\t\"Int32\" : null, \
+\t\t\"Int64\" : null, \
+\t\t\"Uint\" : null, \
+\t\t\"Uint8\" : null, \
+\t\t\"Uint16\" : null, \
+\t\t\"Uint32\" : null, \
+\t\t\"Uint64\" : null, \
+\t\t\"Float32\" : null, \
+\t\t\"Float64\" : null, \
+\t\t\"String\" : null}`)\
+\
+\tnulls := All{\
+\t\tBool: true,\
+\t\tInt: 2,\
+\t\tInt8: 3,\
+\t\tInt16: 4,\
+\t\tInt32: 5,\
+\t\tInt64: 6,\
+\t\tUint: 7,\
+\t\tUint8: 8,\
+\t\tUint16: 9,\
+\t\tUint32: 10,\
+\t\tUint64: 11,\
+\t\tFloat32: 12.1,\
+ Float64: 13.1,\
+ String: "14"}\
+\
+\terr := Unmarshal(jsonData, &nulls)\
+\tif err != nil {\
+\t\tt.Errorf("Unmarshal of null values failed: %v", err)\
+\t}\
+\tif !nulls.Bool || nulls.Int != 2 || nulls.Int8 != 3 || nulls.Int16 != 4 || nulls.Int32 != 5 || nulls.Int64 != 6 ||\
+\t\tnulls.Uint != 7 || nulls.Uint8 != 8 || nulls.Uint16 != 9 || nulls.Uint32 != 10 || nulls.Uint64 != 11 ||\
+\t\tnulls.Float32 != 12.1 || nulls.Float64 != 13.1 || nulls.String != "14" {\
+\
+\t\tt.Errorf("Unmarshal of null values affected primitives")\
+\t}\
+}\
コアとなるコードの解説
src/pkg/encoding/json/decode.go
の変更
decode.go
の literalStore
関数は、JSONパーサーが null
リテラルを読み取ったときに呼び出されます。
変更前:
switch v.Kind() {
default:
d.saveError(&UnmarshalTypeError{"null", v.Type()})
case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice:
v.Set(reflect.Zero(v.Type()))
}
このコードでは、v.Kind()
が reflect.Interface
, reflect.Ptr
, reflect.Map
, reflect.Slice
のいずれかである場合、v
が指す値はゼロ値に設定されます。これは、これらの型が null
を表現できるためです(例: nil
インターフェース、nil
ポインタ、nil
マップ、nil
スライス)。
しかし、default
ケースでは UnmarshalTypeError
が発生していました。これは、int
, string
, bool
, float
などのプリミティブ型や文字列型に null
をデコードしようとした場合にエラーになることを意味します。
変更後:
switch v.Kind() {
case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice:
v.Set(reflect.Zero(v.Type()))
// otherwise, ignore null for primitives/string
}
default
ケースが削除されました。これにより、reflect.Interface
, reflect.Ptr
, reflect.Map
, reflect.Slice
以外の型に対して null
が与えられた場合、switch
文のどの case
にもマッチせず、何も処理が行われません。結果として、v
が指すGoのフィールドの値は変更されず、JSONの null
値は「スキップ」されることになります。これは、Goのプリミティブ型や文字列型が null
を直接表現できないため、既存の値を保持することがより適切な挙動であるという設計判断に基づいています。
src/pkg/encoding/json/decode_test.go
の追加テスト
TestUnmarshalNulls
は、この変更の意図を明確に示し、その挙動を検証するための新しいテストケースです。
-
jsonData
の定義:jsonData := []byte(`{ "Bool" : null, "Int" : null, // ... (他のプリミティブ型も同様に null) "String" : null}`)
このJSONデータは、Goの
All
構造体の各フィールドに対応するキーを持ち、その値がすべてnull
に設定されています。 -
nulls
構造体の初期化:nulls := All{ Bool: true, Int: 2, // ... (他のプリミティブ型も初期値) String: "14"}
All
構造体のインスタンスnulls
が作成され、すべてのフィールドに非ゼロ(または非空)の初期値が設定されています。これは、Unmarshal
処理によってこれらの値が変更されないことを確認するための基準となります。 -
Unmarshal
の実行:err := Unmarshal(jsonData, &nulls)
jsonData
をnulls
構造体にデコードしようとします。変更前の挙動であれば、ここでUnmarshalTypeError
が発生するはずです。 -
エラーチェック:
if err != nil { t.Errorf("Unmarshal of null values failed: %v", err) }
Unmarshal
がエラーを返さないことを確認します。これは、null
値がプリミティブ型や文字列型にデコードされる際にエラーが発生しなくなったことを検証します。 -
値の検証:
if !nulls.Bool || nulls.Int != 2 || ... || nulls.String != "14" { t.Errorf("Unmarshal of null values affected primitives") }
Unmarshal
実行後も、nulls
構造体の各フィールドの値が初期値のままであることを確認します。これにより、JSONのnull
値がGoのプリミティブ型や文字列型のフィールドを上書きせず、既存の値を保持していることが保証されます。
このテストは、encoding/json
パッケージがJSONの null
値をGoのプリミティブ型や文字列型に対して「スキップ」し、エラーを発生させずに既存の値を保持するという新しい挙動を正確に捉えています。
関連リンク
- Go Issue 2540:
encoding/json: nulls should be ignored for primitives
- このコミットが修正した元の問題報告。 - Gerrit Change 6759043:
encoding/json: skip unexpected null values
- このコミットに対応するGoのコードレビューシステムGerritのエントリ。
参考にした情報源リンク
- JSON (JavaScript Object Notation) - ECMA-404 The JSON Data Interchange Syntax
- Go言語
encoding/json
パッケージドキュメント - Go言語
reflect
パッケージドキュメント - Go言語のJSON処理について - Qiita (一般的なGoのJSON処理に関する情報源として)
- Go言語のreflectパッケージについて - Qiita (一般的なGoのreflectパッケージに関する情報源として)