[インデックス 11017] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおいて、特殊な浮動小数点値(NaN: Not a Number, Infinity: 無限大)がJSONにマーシャリングされるのを防ぐための変更を導入しています。これにより、JSON仕様に準拠しない値が生成されることを避け、より堅牢なJSONエンコーディングを実現します。
コミット
commit c20c09251c37c60356e8457a3c7cb632c30b69b1
Author: Evan Shaw <chickencha@gmail.com>
Date: Tue Jan 3 12:30:18 2012 +1100
encoding/json: don't marshal special float values
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5500084
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c20c09251c37c60356e8457a3c7cb632c30b69b1
元コミット内容
encoding/json: don't marshal special float values
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5500084
変更の背景
JSON (JavaScript Object Notation) は、データ交換のための軽量なフォーマットであり、その仕様は厳密に定義されています。しかし、IEEE 754浮動小数点標準で定義されている NaN (Not a Number) や Infinity (無限大) といった特殊な浮動小数点値は、標準のJSON仕様では直接的な表現方法がありません。
過去には、JavaScriptの JSON.stringify() 関数がこれらの値を null に変換したり、非標準の文字列(例: "NaN", "Infinity")として出力したりする実装が存在しました。しかし、これはJSONの相互運用性や厳密なデータ型定義に問題を引き起こす可能性がありました。特に、JSONパーサーがこれらの非標準の文字列を数値として解釈できない場合、データ損失やエラーの原因となります。
Go言語の encoding/json パッケージは、JSON仕様に厳密に準拠することを目指しています。このコミットの背景には、特殊な浮動小数点値がJSONとして出力された場合に、そのJSONが標準仕様に違反し、他のシステムでのパースに問題が生じることを防ぐという目的があります。これにより、GoのJSONエンコーダが生成するJSONの信頼性と互換性が向上します。
前提知識の解説
JSON (JavaScript Object Notation)
JSONは、人間が読み書きしやすく、機械が解析しやすいデータ交換フォーマットです。JavaScriptのオブジェクトリテラル表記に由来しますが、言語に依存しないデータ形式として広く利用されています。JSONのデータ型には、文字列、数値、真偽値、null、オブジェクト、配列があります。
IEEE 754 浮動小数点標準
ほとんどの現代のコンピュータシステムで浮動小数点数を表現するために使用される国際標準です。この標準では、通常の数値の他に、以下の特殊な値を定義しています。
- NaN (Not a Number): 不定形な結果(例: 0/0、無限大/無限大)を表すために使用されます。
- Infinity (無限大): 数値が表現できる最大値を超える場合(例: 1/0)に、正の無限大 (
+Inf) または負の無限大 (-Inf) として使用されます。
JSONと特殊浮動小数点値の互換性問題
標準のJSON仕様(RFC 8259など)では、NaN や Infinity を直接表現するための構文がありません。JSONの数値は、10進数表記の有限な数値のみを許容します。そのため、これらの特殊な浮動小数点値をJSONに含めようとすると、以下のいずれかの問題が発生します。
- 非標準の文字列として出力:
"NaN"や"Infinity"のような文字列として出力される場合、JSONパーサーがこれを数値として認識できず、文字列として扱ってしまう可能性があります。 - nullへの変換: 一部の実装では
nullに変換されますが、これは元の情報の損失を意味します。 - エラー: 厳密なJSONエンコーダは、これらの値を検出した際にエラーを発生させることがあります。
このコミットは、3番目の「エラー」を発生させるアプローチを採用することで、JSON仕様への厳密な準拠と、意図しないデータ表現を防ぐことを目的としています。
Go言語の encoding/json パッケージ
Go言語の標準ライブラリに含まれるパッケージで、Goのデータ構造とJSONデータの間でマーシャリング(Goの構造体をJSONに変換)およびアンマーシャリング(JSONをGoの構造体に変換)を行う機能を提供します。json.Marshal 関数はGoの値をJSONバイトスライスに変換し、json.Unmarshal 関数はその逆を行います。
技術的詳細
このコミットの主要な変更点は、encoding/json パッケージのエンコーダが浮動小数点値を処理する際に、math.NaN()、math.Inf(-1) (負の無限大)、math.Inf(1) (正の無限大) といった特殊な値を検出した場合に、エラーを発生させるようにしたことです。
具体的には、以下の変更が行われました。
mathパッケージのインポート: 浮動小数点値のチェックのためにmath.IsInfおよびmath.IsNaN関数を使用するため、src/pkg/encoding/json/encode.goおよびsrc/pkg/encoding/json/encode_test.goにimport "math"が追加されました。UnsupportedValueError型の導入: 新しいエラー型UnsupportedValueErrorが定義されました。このエラーは、サポートされていない値(この場合は特殊な浮動小数点値)がマーシャリングされようとしたときに返されます。これにより、どのような値が問題を引き起こしたのかを具体的に示すことができます。type UnsupportedValueError struct { Value reflect.Value Str string } func (e *UnsupportedValueError) Error() string { return "json: unsupported value: " + e.Str }- 浮動小数点値のチェックとエラー発生:
encodeState.reflectValueQuotedメソッド内で、reflect.Float32およびreflect.Float64型の値を処理するロジックが変更されました。- 浮動小数点値
fを取得した後、math.IsInf(f, 0)またはmath.IsNaN(f)を使用して、その値が無限大またはNaNであるかをチェックします。 - もしこれらの特殊な値であった場合、
e.error(&UnsupportedValueError{v, strconv.FormatFloat(f, 'g', -1, v.Type().Bits())})を呼び出して、UnsupportedValueErrorを発生させます。これにより、json.Marshal関数はエラーを返して処理を中断します。 - 通常の有限な浮動小数点値は、これまで通り
strconv.AppendFloatを使用して文字列に変換されます。
- 浮動小数点値
- テストケースの追加:
src/pkg/encoding/json/encode_test.goにTestUnsupportedValuesという新しいテスト関数が追加されました。このテストは、math.NaN(),math.Inf(-1),math.Inf(1)を含むスライスunsupportedValuesを定義し、これらの値をjson.Marshalでマーシャリングしようとします。期待される動作は、UnsupportedValueErrorが返されることです。これにより、変更が正しく機能していることが保証されます。
この変更により、Goの encoding/json パッケージは、JSON仕様に準拠しない特殊な浮動小数点値のエンコーディングを明示的に拒否するようになります。これは、生成されるJSONの品質と互換性を高めるための重要なステップです。
コアとなるコードの変更箇所
src/pkg/encoding/json/encode.go
--- a/src/pkg/encoding/json/encode.go
+++ b/src/pkg/encoding/json/encode.go
@@ -12,6 +12,7 @@ package json
import (
"bytes"
"encoding/base64"
+ "math"
"reflect"
"runtime"
"sort"
@@ -170,6 +171,15 @@ func (e *UnsupportedTypeError) Error() string {
return "json: unsupported type: " + e.Type.String()
}
+type UnsupportedValueError struct {
+ Value reflect.Value
+ Str string
+}
+
+func (e *UnsupportedValueError) Error() string {
+ return "json: unsupported value: " + e.Str
+}
+
type InvalidUTF8Error struct {
S string
}
@@ -290,7 +300,11 @@ func (e *encodeState) reflectValueQuoted(v reflect.Value, quoted bool) {
e.Write(b)
}
case reflect.Float32, reflect.Float64:
- b := strconv.AppendFloat(e.scratch[:0], v.Float(), 'g', -1, v.Type().Bits())
+ f := v.Float()
+ if math.IsInf(f, 0) || math.IsNaN(f) {
+ e.error(&UnsupportedValueError{v, strconv.FormatFloat(f, 'g', -1, v.Type().Bits())})
+ }
+ b := strconv.AppendFloat(e.scratch[:0], f, 'g', -1, v.Type().Bits())
if quoted {
writeString(e, string(b))
} else {
src/pkg/encoding/json/encode_test.go
--- a/src/pkg/encoding/json/encode_test.go
+++ b/src/pkg/encoding/json/encode_test.go
@@ -6,6 +6,7 @@ package json
import (
"bytes"
+ "math"
"reflect"
"testing"
)
@@ -107,3 +108,21 @@ func TestEncodeRenamedByteSlice(t *testing.T) {
t.Errorf(" got %s want %s", result, expect)
}
}
+
+var unsupportedValues = []interface{}{
+ math.NaN(),
+ math.Inf(-1),
+ math.Inf(1),
+}
+
+func TestUnsupportedValues(t *testing.T) {
+ for _, v := range unsupportedValues {
+ if _, err := Marshal(v); err != nil {
+ if _, ok := err.(*UnsupportedValueError); !ok {
+ t.Errorf("for %v, got %T want UnsupportedValueError", v, err)
+ }
+ } else {
+ t.Errorf("for %v, expected error", v)
+ }
+ }
+}
コアとなるコードの解説
src/pkg/encoding/json/encode.go の変更
-
import "math"の追加:mathパッケージは、浮動小数点数に関する数学関数を提供します。特にmath.IsInfとmath.IsNaNは、それぞれ値が無限大であるか、NaNであるかを判定するために使用されます。これらの関数は、特殊な浮動小数点値を検出するために不可欠です。 -
UnsupportedValueError構造体の定義: この新しいエラー型は、encoding/jsonパッケージがマーシャリングできない値を検出した際に、より具体的なエラー情報を提供するために導入されました。Valueフィールドは元のreflect.Valueを保持し、Strフィールドは問題となった値の文字列表現を保持します。これにより、デバッグ時にどの値がエラーの原因となったかを特定しやすくなります。 -
reflect.Float32, reflect.Float64ケースの変更:encodeState.reflectValueQuotedメソッドは、Goの値をJSONにエンコードする際の中心的なロジックの一部です。このメソッドがfloat32またはfloat64型の値を処理する際に、以下の重要な変更が加えられました。f := v.Float():reflect.Valueから実際の浮動小数点値を取得します。if math.IsInf(f, 0) || math.IsNaN(f): ここがこのコミットの核心部分です。取得した浮動小数点値fが、math.IsInf(無限大であるか) またはmath.IsNaN(NaNであるか) のいずれかに該当するかをチェックします。math.IsInf(f, 0)の第2引数0は、正負どちらの無限大もチェックすることを意味します。e.error(&UnsupportedValueError{v, strconv.FormatFloat(f, 'g', -1, v.Type().Bits())}): もし値が無限大またはNaNであった場合、e.errorメソッドを呼び出して、UnsupportedValueErrorを発生させます。strconv.FormatFloatは、エラーメッセージに含めるために、問題の浮動小数点値を文字列に変換します。このエラーの発生により、json.Marshalはエラーを返して処理を中断し、不正なJSONが生成されるのを防ぎます。- この変更により、GoのJSONエンコーダは、JSON仕様に準拠しない浮動小数点値の出力を明示的に拒否するようになります。
src/pkg/encoding/json/encode_test.go の変更
-
import "math"の追加: テストファイルでもmath.NaNやmath.Infを使用して特殊な浮動小数点値を生成するため、mathパッケージがインポートされています。 -
unsupportedValues変数の定義:unsupportedValuesは、テスト対象となる特殊な浮動小数点値(NaN、負の無限大、正の無限大)を含むinterface{}型のスライスです。これにより、これらの値に対するテストを簡潔に記述できます。 -
TestUnsupportedValues関数の追加: このテスト関数は、unsupportedValuesスライス内の各値に対して以下の検証を行います。if _, err := Marshal(v); err != nil: 各値をjson.Marshalでマーシャリングしようとします。この変更の目的はエラーを発生させることなので、errがnilでないことを期待します。if _, ok := err.(*UnsupportedValueError); !ok: 返されたエラーがUnsupportedValueError型であることを確認します。これにより、正しい種類のエラーが返されていることを保証します。t.Errorf(...): 期待されるエラーが返されなかった場合、またはエラーが全く返されなかった場合にテストを失敗させます。
これらのテストケースは、encode.go で行われた変更が意図通りに機能し、特殊な浮動小数点値がJSONにマーシャリングされようとしたときに正しくエラーを発生させることを保証します。
関連リンク
- Go CL 5500084: https://golang.org/cl/5500084 (このコミットに対応するGoのコードレビューシステム上のチェンジリスト)
参考にした情報源リンク
- JSON (JavaScript Object Notation) 公式サイト: https://www.json.org/json-ja.html
- RFC 8259 - The JavaScript Object Notation (JSON) Data Interchange Format: https://datatracker.ietf.org/doc/html/rfc8259 (JSONの最新の標準仕様)
- IEEE 754 - Wikipedia: https://ja.wikipedia.org/wiki/IEEE_754
- Go言語
encoding/jsonパッケージドキュメント: https://pkg.go.dev/encoding/json - Go言語
mathパッケージドキュメント: https://pkg.go.dev/math - Go言語
strconvパッケージドキュメント: https://pkg.go.dev/strconv - Go言語
reflectパッケージドキュメント: https://pkg.go.dev/reflect