Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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.ValueKind()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.goliteralStore 関数は、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 は、この変更の意図を明確に示し、その挙動を検証するための新しいテストケースです。

  1. jsonData の定義:

    jsonData := []byte(`{
    	"Bool"    : null, 
    	"Int"     : null, 
    	// ... (他のプリミティブ型も同様に null)
    	"String"  : null}`)
    

    このJSONデータは、Goの All 構造体の各フィールドに対応するキーを持ち、その値がすべて null に設定されています。

  2. nulls 構造体の初期化:

    nulls := All{
    	Bool:    true,
    	Int:     2,
    	// ... (他のプリミティブ型も初期値)
    	String:  "14"}
    

    All 構造体のインスタンス nulls が作成され、すべてのフィールドに非ゼロ(または非空)の初期値が設定されています。これは、Unmarshal 処理によってこれらの値が変更されないことを確認するための基準となります。

  3. Unmarshal の実行:

    err := Unmarshal(jsonData, &nulls)
    

    jsonDatanulls 構造体にデコードしようとします。変更前の挙動であれば、ここで UnmarshalTypeError が発生するはずです。

  4. エラーチェック:

    if err != nil {
    	t.Errorf("Unmarshal of null values failed: %v", err)
    }
    

    Unmarshal がエラーを返さないことを確認します。これは、null 値がプリミティブ型や文字列型にデコードされる際にエラーが発生しなくなったことを検証します。

  5. 値の検証:

    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のエントリ。

参考にした情報源リンク