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

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

json: better error messages when the ,string option is misused

コミット

コミットハッシュ: b37de7387a32a707dad0ef0305ec686bc263ef24 作者: Brad Fitzpatrick bradfitz@golang.org 日付: Thu Jan 12 14:40:29 2012 -0800

json: better error messages when the ,string option is misused

Fixes #2331

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

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

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

元コミット内容

json: better error messages when the ,string option is misused

Fixes #2331

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

変更の背景

Go言語の標準ライブラリであるencoding/jsonパッケージにおいて、構造体タグの,stringオプションが誤用された際に、デコード時のエラーメッセージが不明瞭であったという問題(Go Issue 2331)が存在していました。

,stringオプションは、Goの構造体フィールドをJSONにエンコード/デコードする際に、そのフィールドの値をJSONの文字列として扱うことを指定します。例えば、Goのint型のフィールドにjson:"age,string"というタグを付けると、JSONでは"age": "30"のように数値が文字列として表現されます。これは、JavaScriptなど、数値の精度に問題がある環境との相互運用性や、JSONの数値が時には文字列として送られてくるような柔軟なデータ形式に対応するために有用です。

しかし、この,stringオプションが不適切に使用された場合、例えばJSONの真偽値(true/false)をGoのstring型にデコードしようとしたり、JSONの数値(123)をGoのstring型にデコードしようとしたりすると、json: cannot unmarshal bool into Go value of type stringのような一般的な型不一致エラーが発生していました。このエラーメッセージだけでは、開発者が問題の根本原因が,stringオプションの誤用にあることを即座に特定することが困難でした。

このコミットは、このような,stringオプションの誤用に対して、より具体的で分かりやすいエラーメッセージを提供することで、開発者がデバッグをより効率的に行えるようにすることを目的としています。

前提知識の解説

  • Goのencoding/jsonパッケージ: Go言語の標準ライブラリの一部であり、JSONデータとGoの構造体(struct)の間でデータを変換(エンコード/デコード)するための機能を提供します。
  • 構造体タグ(Struct Tags): Goの構造体のフィールド宣言に付与される文字列リテラルで、フィールドに関するメタデータを提供します。encoding/jsonパッケージでは、json:"fieldName,option"のような形式で、JSONのキー名やエンコード/デコード時の挙動を制御するために広く利用されます。
  • ,stringオプション: 構造体タグのオプションの一つで、json:"key,string"のように使用されます。このオプションが付与されたフィールドは、JSON上では常に文字列として扱われます。
    • エンコード時(Marshal): Goの数値型や真偽値型が、対応するJSON文字列として出力されます(例: int(123)"123"に、bool(true)"true"に)。
    • デコード時(Unmarshal): JSONの文字列値(例: "123""true")を、Goの対応する数値型や真偽値型に変換します。また、JSONの数値や真偽値そのもの(例: 123true)も、このオプションが付与されていればGoの対応する型に正しくデコードできる柔軟性も持ちます。
    • 誤用の例: このオプションは、JSONのプリミティブ型(数値、真偽値、文字列)をGoの対応する型に、JSON文字列としてデコードすることを意図しています。例えば、JSONのtrueをGoのboolにデコードする際に、"true"というJSON文字列として受け取ることを期待するケースです。しかし、JSONのtrueをGoのstring型にデコードしようとするなど、型が根本的に異なる場合に問題が発生し、以前は不明瞭なエラーメッセージが返されていました。
  • アンマーシャリング(Unmarshaling): JSON形式のデータをGoのプログラム内で扱えるデータ構造(通常は構造体やマップ)に変換するプロセスを指します。
  • UnmarshalTypeError: encoding/jsonパッケージが、JSONの値の型とGoの構造体フィールドの型が一致しない場合に生成するエラーの一種です。
  • errPhase: encoding/jsonパッケージの内部で使用される、JSONのパース処理中に発生する一般的な構文エラーや予期せぬ状況を示すエラーです。

技術的詳細

このコミットの主要な変更は、src/pkg/encoding/json/decode.goファイル内のJSONデコードロジック、特にliteralStore関数のエラーハンドリングの改善にあります。

  1. literalStore関数のシグネチャ変更:

    • 変更前: func (d *decodeState) literalStore(item []byte, v reflect.Value)
    • 変更後: func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool)
    • 新しく追加されたfromQuoted引数は、現在処理しているJSONリテラルが、構造体タグの,stringオプションによってJSON文字列としてラップされたもの(例: JSONの"true"をGoのboolにデコードしようとしている場合)であるかどうかを示すブール値です。
  2. object関数からの呼び出し箇所の変更:

    • object関数内で、JSONオブジェクトのフィールド値をデコードする際にliteralStoreが呼び出されます。
    • ここで、destringという内部変数がtrue(これは、現在のフィールドに,stringオプションが適用されていることを示す)の場合に、literalStorefromQuoted引数にtrueを渡すように変更されました。これにより、literalStore関数は、デコード中の値が,stringオプションの影響を受けているかどうかを認識できるようになります。
  3. literalStore内部でのエラーメッセージの改善:

    • literalStore関数内では、JSONリテラルの種類(真偽値、文字列、数値)に応じてGoの対応する型へのデコードを試みます。
    • デコードに失敗し、かつfromQuotedtrueである場合(つまり、,stringオプションが適用されているにもかかわらず型変換に失敗した場合)、より具体的で分かりやすいエラーメッセージが生成されるようになりました。
    • 具体的には、fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())という形式のエラーメッセージが使用されます。このメッセージは、どの値(%qで表示されるitem)をどのGoの型(%vで表示されるv.Type())にアンマーシャルしようとして失敗したのか、そしてその原因が「,string構造体タグの不正な使用」にあることを明確に示します。
    • fmtパッケージが新しくインポートされています。
  4. テストケースの更新:

    • src/pkg/encoding/json/decode_test.goファイル内のwrongStringTestsというテストケースが更新されました。
    • これらのテストは、,stringオプションが誤用された場合に発生するエラーを検証するためのものです。
    • コミット前は、期待されるエラーメッセージが一般的なものでしたが、コミット後は、上記の新しい詳細なエラーメッセージに更新されました。これにより、変更が正しく機能し、期待されるエラーメッセージが生成されることがテストによって保証されます。
  5. ビルドスクリプトの変更:

    • 多数のsrc/buildscript_*.shファイル(各OS/アーキテクチャ向けのビルドスクリプト)が変更されています。これらの変更は、reflectunicode/utf16encoding/jsonといったパッケージのビルド順序や、ビルドプロセスにおける一時ファイルの生成・コピーのロジックが調整されたことによるものです。これは、JSONデコードロジック自体の変更とは直接関係ありませんが、Goのビルドシステム全体での依存関係の整理や最適化の一環として行われたものと推測されます。

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

  • src/pkg/encoding/json/decode.go:

    • importセクションに"fmt"が追加されました。
    • func (d *decodeState) object(v reflect.Value)関数内で、d.literalStoreの呼び出しが変更され、destringの値に応じてfromQuoted引数にtrueまたはfalseが渡されるようになりました。
      --- a/src/pkg/encoding/json/decode.go
      +++ b/src/pkg/encoding/json/decode.go
      @@ -538,7 +539,7 @@ func (d *decodeState) object(v reflect.Value) {
       		// Read value.
       		if destring {
       			d.value(reflect.ValueOf(&d.tempstr))
      -			d.literalStore([]byte(d.tempstr), subv)
      +			d.literalStore([]byte(d.tempstr), subv, true)
       		} else {
       			d.value(subv)
       		}
      
    • func (d *decodeState) literal(v reflect.Value)関数内で、d.literalStoreの呼び出しが変更され、fromQuoted引数にfalseが渡されるようになりました。
      --- a/src/pkg/encoding/json/decode.go
      +++ b/src/pkg/encoding/json/decode.go
      @@ -571,11 +572,15 @@ func (d *decodeState) literal(v reflect.Value) {
       	d.off--
       	d.scan.undo(op)
      
      -	d.literalStore(d.data[start:d.off], v)
      +	d.literalStore(d.data[start:d.off], v, false)
       }
      
       // literalStore decodes a literal stored in item into v.
      -func (d *decodeState) literalStore(item []byte, v reflect.Value) {
      +//
      +// fromQuoted indicates whether this literal came from unwrapping a
      +// string from the ",string" struct tag option. this is used only to
      +// produce more helpful error messages.
      +func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) {
       	// Check for unmarshaler.
       	wantptr := item[0] == 'n' // null
       	unmarshaler, pv := d.indirect(v, wantptr)
      
    • literalStore関数内で、真偽値、文字列、数値のデコード失敗時にfromQuotedの値に応じてエラーメッセージが分岐するようになりました。
      --- a/src/pkg/encoding/json/decode.go
      +++ b/src/pkg/encoding/json/decode.go
      @@ -601,7 +606,11 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value) {
       		value := c == 't'
       		switch v.Kind() {
       		default:
      -			d.saveError(&UnmarshalTypeError{"bool", v.Type()})
      +			if fromQuoted {
      +				d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()))
      +			} else {
      +				d.saveError(&UnmarshalTypeError{"bool", v.Type()})
      +			}
       		case reflect.Bool:
       			v.SetBool(value)
       		case reflect.Interface:
      @@ -611,7 +620,11 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value) {
       	case '"': // string
       		s, ok := unquoteBytes(item)
       		if !ok {
      -			d.error(errPhase)
      +			if fromQuoted {
      +				d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()))
      +			} else {
      +				d.error(errPhase)
      +			}
       		}
       		switch v.Kind() {
       		default:
      @@ -636,12 +649,20 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value) {
      
       	default: // number
       		if c != '-' && (c < '0' || c > '9') {
      -			d.error(errPhase)
      +			if fromQuoted {
      +				d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()))
      +			} else {
      +				d.error(errPhase)
      +			}
       		}
       		s := string(item)
       		switch v.Kind() {
       		default:
      -			d.error(&UnmarshalTypeError{"number", v.Type()})
      +			if fromQuoted {
      +				d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()))
      +			} else {
      +				d.error(&UnmarshalTypeError{"number", v.Type()})
      +			}
       		case reflect.Interface:
       			n, err := strconv.ParseFloat(s, 64)
       			if err != nil {
      
  • src/pkg/encoding/json/decode_test.go:

    • wrongStringTests内の期待されるエラーメッセージが更新されました。
      --- a/src/pkg/encoding/json/decode_test.go
      +++ b/src/pkg/encoding/json/decode_test.go
      @@ -258,13 +258,10 @@ type wrongStringTest struct {
       	in, err string
       }
      
      -// 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"},
      +	{`{"result":"x"}`, `json: invalid use of ,string struct tag, trying to unmarshal "x" into string`},
      +	{`{"result":"foo"}`, `json: invalid use of ,string struct tag, trying to unmarshal "foo" into string`},
      +	{`{"result":"123"}`, `json: invalid use of ,string struct tag, trying to unmarshal "123" into string`},
       }
      
       // If people misuse the ,string modifier, the error message should be
      
  • src/buildscript_*.sh:

    • これらのファイルでは、reflectunicode/utf16encoding/jsonパッケージのビルド関連のセクションが、ファイルの異なる位置に移動されています。これは、ビルドプロセスの内部的な調整によるもので、機能的な変更ではありません。

コアとなるコードの解説

このコミットの核心は、Goのencoding/jsonパッケージが、構造体タグの,stringオプションの誤用によって発生するデコードエラーに対して、より具体的で診断に役立つエラーメッセージを提供するようになった点です。

以前は、例えばJSONの"true"をGoのint型にデコードしようとした場合など、,stringオプションが意図しない型変換を引き起こした際に、UnmarshalTypeErrorerrPhaseといった一般的なエラーが返されていました。これらのエラーは、問題がどこにあるのかを特定するのに十分な情報を含んでいませんでした。

新しい実装では、literalStore関数にfromQuotedというブール値の引数が追加されました。この引数は、現在デコードしようとしているJSONリテラルが、構造体タグの,stringオプションによって文字列として扱われているかどうかを示します。

literalStore関数内でデコードエラーが発生した場合、fromQuotedtrueであれば、デコーダはエラーが,stringオプションの誤用によるものであると判断し、json: invalid use of ,string struct tag, trying to unmarshal %q into %vという形式の、より詳細なエラーメッセージを生成します。このメッセージは、どのJSON値が、どのGoの型にデコードされようとして失敗したのかを明示し、エラーの原因が,stringタグの不適切な使用にあることを開発者に直接伝えます。

decode_test.gowrongStringTestsの更新は、この新しいエラーメッセージが期待通りに生成されることを保証するためのものです。これにより、開発者は、,stringオプションの誤用に関する問題を迅速に特定し、修正できるようになります。

ビルドスクリプトの変更は、Goのビルドシステムにおけるパッケージの依存関係やビルド順序の内部的な調整を反映しており、encoding/jsonパッケージの機能的な改善とは直接関連しません。

関連リンク

参考にした情報源リンク