[インデックス 15751] ファイルの概要
このコミットは、Go言語の標準ライブラリであるencoding/json
パッケージにおける、JSONデコード時の特定のバグ修正に関するものです。具体的には、固定長配列をデコードターゲットとして使用し、かつ入力JSONに余分なオブジェクト要素が含まれる場合に発生していた「data changing underfoot」エラーを解消します。
コミット
commit cb8aebf19d7291ec0acc7fcfc7d9fd0010f66cdc
Author: Rick Arnold <rickarnoldjr@gmail.com>
Date: Wed Mar 13 14:53:03 2013 -0400
encoding/json: properly handle extra objects with fixed size arrays
If a fixed size array is passed in as the decode target and the JSON
to decode has extra array elements that are objects, then previously
the decoder would return a "data changing underfoot" error.
Fixes #3717.
R=golang-dev, adg, rsc
CC=golang-dev
https://golang.org/cl/7490046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cb8aebf19d7291ec0acc7fcfc7d9fd0010f66cdc
元コミット内容
encoding/json: properly handle extra objects with fixed size arrays
固定長配列がデコードターゲットとして渡され、デコード対象のJSONに余分な配列要素(オブジェクト)が含まれている場合、以前はデコーダが「data changing underfoot」エラーを返していました。
このコミットは、この問題を修正します。
Fixes #3717.
変更の背景
この変更は、Goのencoding/json
パッケージが、特定の条件下でJSONのデコードに失敗し、不正確なエラーを返すバグを修正するために行われました。具体的には、Goの固定長配列(例: var arr [0]interface{}
)をJSONデコードのターゲットとして指定し、かつ入力JSONがその固定長配列のサイズを超える要素(特にオブジェクト)を含んでいる場合に問題が発生していました。
本来、Goのencoding/json
デコーダは、ターゲットのデータ構造に収まらない余分なJSON要素を無視して処理を続行するべきです。しかし、このバグが存在する間は、デコーダの内部状態が予期せず変更されたと判断し、「data changing underfoot」(足元でデータが変化している)というエラーを誤って報告していました。これは、デコーダがJSONのパース中に、予期しないトークンシーケンスに遭遇した際に、内部的なスキャン状態が矛盾していると判断した結果です。
この問題は、GoのIssue #3717として報告されており、このコミットはその問題を解決するために実装されました。この修正により、開発者は固定長配列をより柔軟にJSONデコードのターゲットとして利用できるようになり、不必要なエラーに悩まされることがなくなります。
前提知識の解説
1. Go言語のencoding/json
パッケージ
Go言語の標準ライブラリであるencoding/json
パッケージは、Goのデータ構造とJSONデータの間で変換(エンコード/デコード)を行うための機能を提供します。
- エンコード (Marshal): Goの構造体やマップなどのデータをJSON形式のバイト列に変換します。
- デコード (Unmarshal): JSON形式のバイト列をGoのデータ構造(構造体、マップ、スライス、配列など)に変換します。
json.Unmarshal
関数は、JSONデータをGoの指定されたインターフェースや構造体にマッピングする際に、厳密なマッチングを要求しません。つまり、JSONデータがGoのターゲット構造体よりも多くのフィールドを持っていても、それらの余分なフィールドは通常無視されます。同様に、JSON配列がGoのターゲット配列/スライスよりも多くの要素を持っていても、余分な要素は通常無視されるべきです。
2. Go言語の配列とスライス
- 配列 (Array): Goの配列は、固定長で型が同じ要素のシーケンスです。宣言時にサイズが決定され、実行時に変更することはできません。例:
var a [5]int
は5つの整数を保持する配列です。 - スライス (Slice): スライスは、配列の上に構築された動的なビューです。長さは可変で、実行時に要素を追加したり削除したりできます。スライスは、基になる配列の一部を参照します。例:
var s []int
は整数のスライスです。
このコミットで問題となっているのは、特に固定長配列をデコードターゲットとして使用した場合の挙動です。
3. JSONのオブジェクトと配列
- JSONオブジェクト: キーと値のペアの順序なしのコレクションです。
{ "key": "value", "anotherKey": 123 }
のように表現されます。 - JSON配列: 値の順序付きリストです。
[ "value1", 123, { "key": "value" } ]
のように表現されます。配列の要素は、プリミティブ型、文字列、オブジェクト、または別の配列など、任意のJSON値になり得ます。
このバグは、JSON配列の要素としてオブジェクトが含まれている場合に顕著に現れていました。
4. 「data changing underfoot」エラー
これは、Goのencoding/json
パッケージの内部で発生するエラーメッセージです。このエラーは、デコーダがJSONストリームをパースしている最中に、その内部状態(特にscan
構造体によって管理されるパースの状態)が予期しない方法で変更された、または矛盾した状態になったと判断した場合に発生します。
通常、これはデコーダがJSONの構文規則に違反する入力に遭遇したか、あるいはデコーダ自身のロジックにバグがあり、内部状態の遷移が正しく行われなかった場合に発生します。このコミットのケースでは、後者のデコーダのロジックのバグが原因でした。デコーダが固定長配列の終端に達した後も、JSONストリームに余分なオブジェクト要素が続いている場合、デコーダはそれらの要素を適切にスキップできず、内部状態が混乱し、このエラーを吐き出していました。
技術的詳細
この修正は、encoding/json
パッケージのデコード処理の中核を担うdecodeState
構造体のvalue
メソッドに焦点を当てています。value
メソッドは、JSONの値をGoのreflect.Value
にデコードする役割を担っています。
問題の根本原因は、デコーダがJSON文字列("
で囲まれた値)を読み込んだ後、その直後にJSONオブジェクトのキーを読み込んだと誤解釈する可能性があった点にあります。これは、デコーダの内部スキャナー(d.scan
)が、パース状態スタック(d.scan.parseState
)にparseObjectKey
という状態を誤って残してしまうことが原因でした。
具体的には、JSON配列の要素としてオブジェクト{}
が存在し、かつデコードターゲットが固定長配列で、その固定長配列が既に満たされている(つまり、これ以上要素を受け入れない)場合、デコーダは余分なオブジェクトをスキップしようとします。しかし、オブジェクトのスキップ処理中に、デコーダが文字列を読み込んだ直後に、内部状態が「オブジェクトキーを読んだ」と誤認してしまうことがありました。
この誤認が発生すると、デコーダはオブジェクトのキーと値のペアを期待し、それに続くJSONトークンがその期待に沿わない場合に「data changing underfoot」エラーを発生させていました。
修正は、この誤認を検出し、デコーダの内部状態を適切にリセットすることによって行われます。
修正のロジック
追加されたコードは、d.scan.step(&d.scan, '"')
が2回連続で呼び出された直後(これはJSON文字列の開始と終了を処理した後によく見られるパターン)に実行されます。
n := len(d.scan.parseState)
: 現在のパース状態スタックの深さを取得します。if n > 0 && d.scan.parseState[n-1] == parseObjectKey
:- スタックが空でないこと (
n > 0
)。 - スタックの最上位の状態が
parseObjectKey
であること。 この2つの条件が同時に真である場合、デコーダは以前にオブジェクトキーを読み込んだと誤解している状態にあると判断されます。
- スタックが空でないこと (
- この誤解釈を修正するために、デコーダは以下のステップを強制的に実行します。
d.scan.step(&d.scan, ':')
: オブジェクトキーの後に続くコロンをスキップします。d.scan.step(&d.scan, '"')
: オブジェクトの値として文字列の開始クォートをスキップします。d.scan.step(&d.scan, '"')
: オブジェクトの値として文字列の終了クォートをスキップします。d.scan.step(&d.scan, '}')
: オブジェクトの終了ブレースをスキップします。
これらのステップは、デコーダが「オブジェクトキーを読んだ」と誤認した状態から、あたかも空の文字列値を持つオブジェクトキーを完全にパースし終えたかのように内部状態を遷移させます。これにより、デコーダの内部状態が整合性の取れた状態に戻り、後続のJSONパースが正しく行われるようになります。結果として、「data changing underfoot」エラーが回避されます。
この修正は、特に固定長配列がデコードターゲットであり、JSON入力に余分なオブジェクト要素が含まれるという、特定のコーナーケースに対応しています。
コアとなるコードの変更箇所
src/pkg/encoding/json/decode.go
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -261,6 +261,16 @@ func (d *decodeState) value(v reflect.Value) {
}
d.scan.step(&d.scan, '"')
d.scan.step(&d.scan, '"')
+
+ n := len(d.scan.parseState)
+ if n > 0 && d.scan.parseState[n-1] == parseObjectKey {
+ // d.scan thinks we just read an object key; finish the object
+ d.scan.step(&d.scan, ':')
+ d.scan.step(&d.scan, '"')
+ d.scan.step(&d.scan, '"')
+ d.scan.step(&d.scan, '}')
+ }
+
return
}
src/pkg/encoding/json/decode_test.go
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -1178,3 +1178,16 @@ func TestUnmarshalJSONLiteralError(t *testing.T) {
t.Errorf("got err = %v; want out of range error", err)
}
}
+
+// Test that extra object elements in an array do not result in a
+// "data changing underfoot" error.
+// Issue 3717
+func TestSkipArrayObjects(t *testing.T) {
+ json := `[{}]`
+ var dest [0]interface{}
+
+ err := Unmarshal([]byte(json), &dest)
+ if err != nil {
+ t.Errorf("got error %q, want nil", err)
+ }
+}
コアとなるコードの解説
src/pkg/encoding/json/decode.go
の変更
decode.go
のvalue
メソッド内の変更は、JSONデコードの内部状態管理を改善するためのものです。
既存のコードでは、JSON文字列を読み込んだ後(d.scan.step(&d.scan, '"')
が2回呼び出された後)、デコーダの内部スキャン状態が誤ってparseObjectKey
(オブジェクトのキーをパース中)と認識される可能性がありました。これは、特にJSON配列内にオブジェクトが存在し、そのオブジェクトがデコードターゲットの固定長配列の容量を超えている場合に問題となりました。
追加されたコードブロックは、この誤った状態を検出し、修正します。
n := len(d.scan.parseState)
: 現在のパース状態スタックの深さを取得します。if n > 0 && d.scan.parseState[n-1] == parseObjectKey
:n > 0
: パース状態スタックが空でないことを確認します。d.scan.parseState[n-1] == parseObjectKey
: スタックの最上位がparseObjectKey
である、つまりデコーダがオブジェクトキーを読み込んだと誤解している状態であることを確認します。
- この条件が真の場合、デコーダは誤った状態にあるため、強制的にオブジェクトの残りの部分(コロン、空の文字列値、閉じブレース)をスキップします。これにより、デコーダの内部状態が整合性の取れた状態に戻り、後続のパースが正しく行われるようになります。この「スキップ」は、実際には
d.scan.step
を呼び出すことで、スキャナーを次の適切な状態に強制的に進めることを意味します。
この修正により、デコーダは余分なJSONオブジェクト要素を適切に無視し、不必要な「data changing underfoot」エラーを発生させなくなります。
src/pkg/encoding/json/decode_test.go
の変更
decode_test.go
に追加されたTestSkipArrayObjects
テストケースは、このバグが修正されたことを検証するためのものです。
json :=
[{}]``: テスト用のJSON文字列を定義します。これは、1つの空のオブジェクトを含むJSON配列です。var dest [0]interface{}
: デコードターゲットとして、サイズが0の固定長配列を定義します。interface{}
型は、任意の型の値を保持できるため、JSONの任意の値をデコードするのに適しています。err := Unmarshal([]byte(json), &dest)
: 定義したJSON文字列をdest
配列にデコードしようとします。if err != nil { t.Errorf("got error %q, want nil", err) }
:- このテストの目的は、デコードがエラーなく成功することを確認することです。
- もしエラーが発生した場合(特に修正前の「data changing underfoot」エラー)、テストは失敗します。
- エラーが
nil
(エラーなし)であれば、テストは成功し、バグが修正されたことが確認されます。
このテストケースは、固定長配列がデコードターゲットであり、JSON入力に余分なオブジェクト要素が含まれるという、まさにこのコミットが修正しようとしているシナリオを正確に再現しています。
関連リンク
- Go Issue #3717: https://github.com/golang/go/issues/3717
- Go CL 7490046: https://golang.org/cl/7490046 (これはコミットメッセージに記載されているGoのコードレビューシステムへのリンクです)
参考にした情報源リンク
- Go
encoding/json
パッケージのドキュメント: https://pkg.go.dev/encoding/json - Go言語の配列とスライスに関する公式ドキュメントやチュートリアル
- JSONの仕様に関する情報 (RFC 8259など)
- Goの
reflect
パッケージに関する情報 (デコード処理でreflect.Value
が使用されているため) - GoのIssueトラッカーやコードレビューシステムでの関連議論 (Issue #3717など)
- "data changing underfoot" エラーに関するGoコミュニティでの議論やStack Overflowの質問など