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

[インデックス 10816] ファイルの概要

コミット

  • コミットハッシュ: f89b5746fb809bef08eec46760ce429f420435fa
  • 作者: Brad Fitzpatrick bradfitz@golang.org
  • 日付: Thu Dec 15 10:02:47 2011 -0800

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/f89b5746fb809bef08eec46760ce429f420435fa

元コミット内容

json: some tests to demonstrate bad error messages

Not a fix yet (help wanted), but part of Issue 2331

R=rsc
CC=golang-dev
https://golang.org/cl/5490043

変更の背景

このコミットは、Go言語の標準ライブラリであるencoding/jsonパッケージにおける、JSONデコード時のエラーメッセージの品質に関する問題提起を目的としています。具体的には、jsonタグの,stringオプションが誤用された場合に、ユーザーにとって理解しにくい、あるいは誤解を招くようなエラーメッセージが生成される現状をテストコードとして明確に示しています。

これは、GoのIssue 2331「json.unmarshal unmarshals an invalid json in some cases」の一部として行われたもので、直接的な修正ではなく、問題の再現と可視化に焦点を当てています。開発者は、このテストを通じて、より有用なエラーメッセージの必要性を強調し、コミュニティからの改善協力を求めています。

前提知識の解説

Go言語のencoding/jsonパッケージ

encoding/jsonパッケージは、GoプログラムとJSONデータの間で変換(エンコードとデコード)を行うための標準ライブラリです。Goの構造体とJSONオブジェクトの間でデータをマッピングする機能を提供し、WebアプリケーションやAPI開発において広く利用されています。

  • エンコード (Marshal): Goのデータ構造(構造体、スライス、マップなど)をJSON形式のバイト列に変換します。
  • デコード (Unmarshal): JSON形式のバイト列をGoのデータ構造に変換します。この際、json.Unmarshal関数やjson.Decoder型が使用されます。

json構造体タグと,stringオプション

Goの構造体フィールドには、json:"fieldName,options"のようなタグを付与することで、JSONとのマッピング方法をカスタマイズできます。

  • fieldName: JSONにおけるフィールド名を指定します。
  • options: 追加のデコード/エンコードオプションを指定します。
    • ,string: このオプションは、通常、JSONの数値やブール値をGoの文字列型フィールドにデコードする際に使用されます。例えば、JSONの{"value": 123}をGoのstruct { Value string json:"value,string" }にデコードすると、Valueフィールドには文字列"123"が格納されます。このオプションは、JSONのデータ型とGoのフィールド型が直接一致しない場合に、型変換を試みるためのヒントとして機能します。しかし、このコミットが示すように、string型のフィールドにstring型のJSON値をデコードする際に,stringオプションを付与すると、意図しない挙動や誤解を招くエラーが発生する可能性があります。

JSONデコード時のエラーハンドリング

encoding/jsonパッケージは、不正なJSON入力や型不一致が発生した場合にエラーを返します。理想的なエラーメッセージは、問題の原因を明確に示し、開発者が迅速に問題を特定し修正できるようにするものです。しかし、複雑なデコードシナリオや特定のタグの誤用においては、エラーメッセージが抽象的であったり、根本原因を指し示さない場合があります。

Goのテストフレームワーク (testingパッケージ)

Goには、標準でtestingパッケージが提供されており、ユニットテストやベンチマークテストを記述できます。

  • テスト関数の命名: TestXxxという形式で関数名を定義します(例: TestErrorMessageFromMisusedString)。
  • *testing.T: テスト関数に渡される引数で、テストの失敗を報告したり、ログを出力したりするためのメソッドを提供します。
  • t.Errorf(...): テストが失敗したことを報告し、指定されたフォーマットでエラーメッセージを出力します。テストの実行は継続されます。
  • fmt.Sprintf: フォーマットされた文字列を生成するために使用されます。このコミットでは、実際のエラーメッセージを文字列として取得し、期待されるエラーメッセージと比較するために使用されています。

技術的詳細

このコミットは、encoding/jsonパッケージのDecoder.Decodeメソッドが、特定の不正なJSON入力に対して生成するエラーメッセージの不備を浮き彫りにしています。

GoのIssue 2331は、「json.unmarshal unmarshals an invalid json in some cases」と題されており、json.Decoderの動作特性に起因する問題が指摘されています。json.DecoderはストリーミングJSONデータを処理するように設計されており、入力ストリームから有効なJSON値を見つけると、その値をデコードして返します。この特性のため、入力ストリームの残りの部分が不正であっても、最初の有効なJSON値がデコードされてしまうことがあります。これは、encoding/jsonパッケージ自体のバグというよりは、Decoderの設計上の挙動であり、堅牢なエラーハンドリングが求められる場合には、開発者がこの特性を考慮してコードを記述する必要があります。

このコミットでは、特にjson:"result,string"というタグをstring型のフィールドに適用した場合の挙動に焦点を当てています。通常、,stringオプションは数値やブール値を文字列としてデコードするために使用されますが、既にstring型であるフィールドに適用すると、デコーダは予期しない内部状態に陥り、結果として「JSON decoder out of sync - data changing underfoot?」のような、ユーザーにとって意味不明なエラーメッセージを生成することがあります。また、JSONの型(数値やブール値)がGoのstring型フィールドにデコードされる際に、",string"オプションが指定されていない場合と同様に型不一致エラーが発生しますが、そのメッセージも必ずしも直感的ではありません。

このコミットで追加されたテストは、これらのシナリオを具体的に再現し、現状のエラーメッセージがどれほど不親切であるかを示しています。これは、将来的にencoding/jsonパッケージのエラーメッセージを改善するための基礎データとなります。

コアとなるコードの変更箇所

変更はsrc/pkg/encoding/json/decode_test.goファイルに対して行われています。

--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -6,6 +6,7 @@ package json

 import (
 	"bytes"
+	"fmt"
 	"reflect"
 	"strings"
 	"testing"
@@ -242,6 +243,38 @@ func TestHTMLEscape(t *testing.T) {
 	}\n}\n\n+// WrongString is a struct that's misusing the ,string modifier.
+type WrongString struct {
+	Message string `json:"result,string"`
+}\n+\n+type wrongStringTest struct {
+	in, err string
+}\n+\n+// TODO(bradfitz): as part of Issue 2331, fix these tests' expected
+// error values to be helpful, rather than the confusing messages they
+// are now.
+var wrongStringTests = []wrongStringTest{
+	{`{"result":"x"}`, "JSON decoder out of sync - data changing underfoot?"},
+	{`{"result":"foo"}`, "json: cannot unmarshal bool into Go value of type string"},
+	{`{"result":"123"}`, "json: cannot unmarshal number into Go value of type string"},
+}\n+\n+// If people misuse the ,string modifier, the error message should be
+// helpful, telling the user that they're doing it wrong.
+func TestErrorMessageFromMisusedString(t *testing.T) {
+	for n, tt := range wrongStringTests {
+		r := strings.NewReader(tt.in)
+		var s WrongString
+		err := NewDecoder(r).Decode(&s)
+		got := fmt.Sprintf("%v", err)
+		if got != tt.err {
+			t.Errorf("%d. got err = %q, want %q", n, got, tt.err)
+		}
+	}
+}\n+\n func noSpace(c rune) rune {
 	if isSpace(c) {
 		return -1

主な変更点は以下の通りです。

  1. fmtパッケージのインポート: エラーメッセージをフォーマットするためにfmtパッケージが追加されました。
  2. WrongString構造体の定義:
    type WrongString struct {
    	Message string `json:"result,string"`
    }
    
    Messageフィールドがstring型であるにもかかわらず、json:"result,string"タグが付与されています。これが誤用を意図した構造体です。
  3. wrongStringTest構造体とwrongStringTestsスライスの定義:
    type wrongStringTest struct {
    	in, err string
    }
    
    var wrongStringTests = []wrongStringTest{
    	{`{"result":"x"}`, "JSON decoder out of sync - data changing underfoot?"},
    	{`{"result":"foo"}`, "json: cannot unmarshal bool into Go value of type string"},
    	{`{"result":"123"}`, "json: cannot unmarshal number into Go value of type string"},
    }
    
    inは入力JSON文字列、errは期待される(現状の)エラーメッセージです。異なる不正な入力パターンと、それに対応するエラーメッセージが定義されています。
  4. TestErrorMessageFromMisusedString関数の追加:
    func TestErrorMessageFromMisusedString(t *testing.T) {
    	for n, tt := range wrongStringTests {
    		r := strings.NewReader(tt.in)
    		var s WrongString
    		err := NewDecoder(r).Decode(&s)
    		got := fmt.Sprintf("%v", err)
    		if got != tt.err {
    			t.Errorf("%d. got err = %q, want %q", n, got, tt.err)
    		}
    	}
    }
    
    このテスト関数はwrongStringTestsスライスをループし、各テストケースに対して以下の処理を行います。
    • strings.NewReader(tt.in)で入力JSON文字列からio.Readerを作成します。
    • NewDecoder(r).Decode(&s)でJSONデコードを試みます。
    • fmt.Sprintf("%v", err)でエラーオブジェクトを文字列に変換します。
    • 変換されたエラー文字列gotが、期待されるエラー文字列tt.errと一致しない場合にt.Errorfでテスト失敗を報告します。

コアとなるコードの解説

このコミットの核心は、TestErrorMessageFromMisusedStringテスト関数とその関連データ構造にあります。

WrongString構造体は、json:"result,string"タグがstring型のフィールドMessageに適用されているという、意図的な「誤用」を示しています。通常、,stringオプションはJSONの数値やブール値をGoの文字列としてデコードするために使われます。しかし、JSONのstring値をGoのstringフィールドにデコードする際にこのオプションを使うと、デコーダの内部ロジックが混乱し、予期しないエラーパスに分岐することがあります。

wrongStringTestsスライスは、この誤用が引き起こす具体的なエラーシナリオを網羅しています。

  1. {"result":"x"}, "JSON decoder out of sync - data changing underfoot?"}:
    • JSONの"x"は有効な文字列ですが、,stringオプションの誤用により、デコーダが内部状態を失い、「データが足元で変化している」というような、非常に抽象的で役に立たないエラーメッセージを生成するケースです。これは、デコーダが期待する形式(数値やブール値の文字列表現)ではない文字列を受け取った際に発生する可能性のある、内部的な不整合を示唆しています。
  2. {"result":"foo"}, "json: cannot unmarshal bool into Go value of type string"}:
    • JSONの"foo"は文字列ですが、デコーダが,stringオプションを解釈しようとする際に、内部的にブール値への変換を試み、それが失敗したかのようなエラーメッセージを生成しています。これは、,stringオプションが数値やブール値のデコードに特化しているため、文字列リテラルに対して適用された場合に、デコーダが誤った型変換ロジックを適用しようとする結果と考えられます。
  3. {"result":"123"}, "json: cannot unmarshal number into Go value of type string"}:
    • JSONの"123"は文字列ですが、デコーダがこれを数値として解釈しようとし、Goのstring型フィールドに数値としてアンマーシャルできないというエラーを返しています。これは、,stringオプションが数値の文字列表現を期待しているにもかかわらず、JSONの文字列リテラルが直接渡された場合に、デコーダがその文字列を数値としてパースしようとする挙動を示唆しています。

TestErrorMessageFromMisusedString関数は、これらのテストケースを自動的に実行し、実際のエラーメッセージがwrongStringTestsで定義された「期待される(現状の)エラーメッセージ」と一致するかを確認します。もし一致しない場合、テストは失敗し、開発者はエラーメッセージの変更を検知できます。

このテストの目的は、これらのエラーメッセージが「helpful(役に立つ)」ではないことを明確に示し、GoのIssue 2331で議論されているように、より診断的でユーザーフレンドリーなエラーメッセージへの改善を促すことです。TODO(bradfitz)コメントは、このテストが修正ではなく、問題の特定と将来の改善のためのものであることを明示しています。

関連リンク

参考にした情報源リンク