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

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

このコミットは、Go言語の標準ライブラリである encoding/json パッケージにおける、バイトスライス([]byte)型のJSONアンマーシャリングの挙動を修正するものです。具体的には、net.IP のように内部的にバイトスライスとして表現されるカスタム型が、JSON文字列から正しくデコード(アンマーシャル)できるように改善されています。

コミット

commit 59306493067a6ebcc50bc9dfd4a1d1af543bd2d8
Author: Paul Borman <borman@google.com>
Date:   Thu Jul 11 22:34:09 2013 -0400

    json: unmarshal types that are byte slices.
    
    The json package cheerfully would marshal
    
            type S struct {
                    IP net.IP
            }
    
    but would give an error when unmarshalling.  This change allows any
    type whose concrete type is a byte slice to be unmarshalled from a
    string.
    
    Fixes #5086.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/11161044

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

https://github.com/golang/go/commit/59306493067a6ebcc50bc9dfd4a1d1af543bd2d8

元コミット内容

json: バイトスライスである型をアンマーシャルする。

jsonパッケージは、以下のような構造体を喜んでマーシャル(JSONエンコード)していました。

type S struct {
        IP net.IP
}

しかし、アンマーシャル(JSONデコード)しようとするとエラーが発生していました。この変更により、具象型がバイトスライスである任意の型が、JSON文字列からアンマーシャルできるようになります。

Fixes #5086.

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

変更の背景

Goの encoding/json パッケージは、Goのデータ構造とJSONデータの間で変換を行うための重要なツールです。このコミットが修正しようとしている問題は、特定のGoの型、特に net.IP のように基盤となる具象型が []byte であるカスタム型が、JSONのマーシャリング(GoからJSONへの変換)はできるものの、アンマーシャリング(JSONからGoへの変換)ができないという非対称な挙動でした。

具体的には、net.IP 型のフィールドを持つ構造体をJSONにエンコードすると、net.IP の値はBase64エンコードされた文字列として出力されます。これは []byte 型のデフォルトのマーシャリング挙動です。しかし、そのJSON文字列を元のGoの構造体にデコードしようとすると、encoding/json パッケージはエラーを返していました。これは、[]byte 型(またはそのエイリアス)をJSON文字列からデコードする際の内部ロジックに不備があったためです。

この非対称性は、開発者にとって混乱を招き、net.IP のような一般的な型をJSONで扱う際の障壁となっていました。このコミットは、この問題を解決し、[]byte を基盤とする型がJSON文字列から一貫してアンマーシャルできるようにすることで、encoding/json パッケージの堅牢性と使いやすさを向上させることを目的としています。

前提知識の解説

このコミットの理解には、以下のGo言語の概念と encoding/json パッケージの動作に関する知識が不可欠です。

  1. JSONマーシャリングとアンマーシャリング:

    • マーシャリング (Marshaling): Goのデータ構造(構造体、スライス、マップなど)をJSON形式のバイトスライスに変換するプロセスです。json.Marshal() 関数がこれを行います。
    • アンマーシャリング (Unmarshaling): JSON形式のバイトスライスをGoのデータ構造に変換するプロセスです。json.Unmarshal() 関数がこれを行います。
    • encoding/json パッケージは、Goの型とJSONの型の間の標準的なマッピングを提供します。例えば、Goの文字列はJSONの文字列に、Goの数値はJSONの数値に、Goのブール値はJSONのブール値に、Goのスライスや配列はJSONの配列に、Goの構造体はJSONのオブジェクトにそれぞれ対応します。
  2. reflect パッケージ:

    • Goの reflect パッケージは、実行時にプログラムの構造を検査(リフレクション)するための機能を提供します。これにより、変数の型、値、フィールド、メソッドなどを動的に調べることができます。
    • reflect.Value: 実行時のGoの変数の値を表します。
    • reflect.Type: 実行時のGoの変数の型を表します。
    • reflect.Kind: 型の基本的なカテゴリ(例: reflect.Int, reflect.String, reflect.Slice, reflect.Struct など)を表す列挙型です。
    • v.Type(): reflect.Value からその値の reflect.Type を取得します。
    • v.Type().Elem(): ポインタ、配列、スライス、マップ、チャネルなどの型の場合、その要素の型(reflect.Type)を返します。例えば、[]byteElem()byte (つまり uint8) の型を返します。
    • v.Type().Elem().Kind(): 要素の型の Kind を取得します。このコミットでは、スライスの要素が uint8 (つまり byte) であるかどうかを判定するために使用されます。
  3. []byte 型の特殊な扱い:

    • encoding/json パッケージでは、[]byte 型はJSONの配列ではなく、Base64エンコードされたJSON文字列としてマーシャルされます。これは、バイナリデータをJSONに埋め込む際の一般的な慣習です。
    • 例えば、[]byte{0x01, 0x02, 0x03}AQID というBase64文字列としてJSONにエンコードされます。
  4. net.IP:

    • Goの標準ライブラリ net パッケージにある IP 型は、IPアドレスを表すために使用されます。
    • net.IP は、実際には []byte のエイリアス(type IP []byte)として定義されています。
    • このため、net.IPencoding/json パッケージによって []byte と同様に扱われ、マーシャリング時にはBase64エンコードされた文字列になります。しかし、アンマーシャリング時には、このコミット以前は問題がありました。
  5. UnmarshalTypeError:

    • encoding/json パッケージが、JSONのデータ型とGoのターゲットのデータ型の間で互換性のない変換を検出した場合に返すエラーです。
    • 例えば、JSONの数値 123 をGoの string 型にデコードしようとすると、UnmarshalTypeError が発生します。

これらの概念を理解することで、コミットが encoding/json パッケージの内部でどのように reflect を利用して型を検査し、[]byte 型のアンマーシャリングの挙動を修正しているのかを深く把握することができます。

技術的詳細

このコミットの核心は、encoding/json パッケージがJSON文字列をGoのデータ構造にアンマーシャルする際の、reflect.Slice 型(特に []byte 型)の処理ロジックの変更にあります。

encoding/json パッケージの内部では、decodeState 構造体がJSONデコードの状態を管理し、literalStore メソッドがJSONのプリミティブ値(文字列、数値、ブール値など)をGoの reflect.Value に格納する役割を担っています。

コミット前の literalStore メソッドでは、JSON文字列をGoの reflect.Slice 型にデコードしようとする際、そのスライスが byteSliceType(つまり []byte 型)と完全に一致するかどうかを v.Type() != byteSliceType という比較で判定していました。

このアプローチには問題がありました。net.IP のように []byte のエイリアスとして定義されたカスタム型は、reflect.Type の観点からは []byte とは異なる型として扱われます。例えば、type MyBytes []byte と定義した場合、MyBytesreflect.Type[]bytereflect.Type とは異なります。したがって、v.Type() != byteSliceType のチェックは、net.IPMyBytes のような []byte のエイリアス型に対しては true を返し、結果として UnmarshalTypeError を発生させていました。これは、「JSON文字列をGoの文字列型にデコードしようとしたが、ターゲットはスライス型だった」という誤ったエラーメッセージを伴うことがありました。

このコミットでは、この判定ロジックを v.Type().Elem().Kind() != reflect.Uint8 に変更しています。

  • v.Type(): デコード対象のGoの型の reflect.Type を取得します。
  • Elem(): スライス型の場合、その要素の型(reflect.Type)を取得します。
  • Kind(): 要素の型の reflect.Kind を取得します。

この変更により、literalStore メソッドは、スライスの要素の具象型が uint8(つまり byte)であるかどうかをチェックするようになりました。[]byte 型も net.IP 型も、その要素の型は byte であり、その Kindreflect.Uint8 です。したがって、この新しいチェックは、[]byte そのものだけでなく、[]byte を基盤とするすべてのカスタム型(エイリアス型)を正しく識別し、JSON文字列からデコードできるようになります。

これにより、net.IP のような型がJSON文字列から正しくアンマーシャルされるようになり、マーシャリングとアンマーシャリングの間の非対称性が解消されました。

encode.go の変更は、機能的な変更ではなく、[]byte のマーシャリングに関するコメントの記述をより正確にするためのものです。以前の「Array and slice values encode as JSON arrays, except that []byte encodes as a base64-encoded string」という記述を、「Array and slice values encode as JSON arrays, except that a slice of bytes encodes as a base64-encoded string」と修正し、より明確に「バイトのスライス」であることを強調しています。これは、[]byte が配列ではなくスライスであることを明確にし、読者の誤解を防ぐための改善です。

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

このコミットによる主要なコード変更は以下の3つのファイルにわたります。

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

    • func (d *decodeState) literalStore(...) メソッド内の、reflect.Slice 型を処理する case 文が変更されました。
    • 変更前: if v.Type() != byteSliceType {
    • 変更後: if v.Type().Elem().Kind() != reflect.Uint8 {
  2. src/pkg/encoding/json/decode_test.go:

    • TestByteSliceType という新しいテスト関数が追加されました。
    • このテストは、[]byte のエイリアス型(type A []byte)を持つ構造体 S を定義し、そのインスタンスをマーシャルおよびアンマーシャルできることを検証します。
    • net.IP のような型が正しく扱われることを間接的に保証します。
  3. src/pkg/encoding/json/encode.go:

    • json パッケージのエンコードに関するコメントが修正されました。
    • 変更前: // Array and slice values encode as JSON arrays, except that\n// []byte encodes as a base64-encoded string, and a nil slice
    • 変更後: // Array and slice values encode as JSON arrays, except that a slice of\n// bytes encodes as a base64-encoded string, and a nil slice
    • これは機能的な変更ではなく、ドキュメントの明確化です。

コアとなるコードの解説

src/pkg/encoding/json/decode.go の変更

--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -660,7 +660,7 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool
 		default:
 			d.saveError(&UnmarshalTypeError{"string", v.Type()})
 		case reflect.Slice:
-			if v.Type() != byteSliceType {
+			if v.Type().Elem().Kind() != reflect.Uint8 {
 				d.saveError(&UnmarshalTypeError{"string", v.Type()})
 				break
 			}

この変更は、JSON文字列をGoの reflect.Slice 型にデコードする際の、型チェックのロジックを改善しています。

  • 変更前 (if v.Type() != byteSliceType):

    • byteSliceTypereflect.Type の変数で、[]byte 型の reflect.Type を保持していました。
    • この条件は、デコード対象の reflect.Value v の具象型が、厳密に []byte 型と一致しない場合にエラーを発生させていました。
    • net.IP のように []byte のエイリアスとして定義された型は、reflect.Type の観点では []byte とは異なる型であるため、このチェックを通過できず、不適切な UnmarshalTypeError が発生していました。
  • 変更後 (if v.Type().Elem().Kind() != reflect.Uint8):

    • v.Type(): vreflect.Type を取得します。例えば、net.IP 型であれば net.IP の型情報です。
    • Elem(): スライス型の場合、その要素の reflect.Type を取得します。net.IP (つまり []byte) の場合、要素は byte なので、byte 型の reflect.Type が返されます。
    • Kind(): 要素の型の reflect.Kind を取得します。byte 型の Kindreflect.Uint8 です。
    • この新しい条件は、「スライスの要素の具象型が uint8(つまり byte)ではない場合」にエラーを発生させるように変更されました。
    • これにより、[]byte そのものだけでなく、net.IP のように []byte を基盤とするすべてのカスタム型が、その要素が byte であるという共通の特性に基づいて正しく識別され、JSON文字列からデコードできるようになりました。

src/pkg/encoding/json/decode_test.go の追加テスト

// Test that types of byte slices (such as net.IP) both
// marshal and unmarshal.
func TestByteSliceType(t *testing.T) {
	type A []byte
	type S struct {
		A A
	}

	for x, in := range []S{
		S{},
		S{A: []byte{'1'}},
		S{A: []byte{'1', '2', '3', '4', '5'}},
	} {
		data, err := Marshal(&in)
		if err != nil {
			t.Errorf("#%d: got Marshal error %q, want nil", x, err)
			continue
		}
		var out S
		err = Unmarshal(data, &out)
		if err != nil {
			t.Fatalf("#%d: got Unmarshal error %q, want nil", x)
		}
		if !reflect.DeepEqual(&out, &in) {
			t.Fatalf("#%d: got %v, want %v", x, &out, &in)
		}
	}
}

この新しいテストケース TestByteSliceType は、このコミットの変更が意図通りに機能することを検証するために追加されました。

  • type A []byte[]byte のエイリアス型 A を定義しています。これは net.IP[]byte のエイリアスである状況を模倣しています。
  • type S struct { A A } という構造体を定義し、このエイリアス型をフィールドに持ちます。
  • 様々な長さのバイトスライスを持つ S のインスタンスを生成し、それぞれに対して以下の操作を行います。
    1. json.Marshal(&in): 構造体をJSONにマーシャルします。A 型のフィールドはBase64エンコードされた文字列としてマーシャルされるはずです。
    2. json.Unmarshal(data, &out): マーシャルされたJSONデータを新しい構造体 out にアンマーシャルします。
    3. reflect.DeepEqual(&out, &in): 元の構造体 in とアンマーシャルされた構造体 out が完全に一致するかどうかを検証します。

このテストが成功することで、[]byte のエイリアス型がJSON文字列から正しくアンマーシャルされ、元のデータが忠実に復元されることが保証されます。

src/pkg/encoding/json/encode.go のコメント変更

--- a/src/pkg/encoding/json/encode.go
+++ b/src/pkg/encoding/json/encode.go
@@ -44,8 +44,8 @@ import (
 // The angle brackets "<" and ">" are escaped to "\u003c" and "\u003e"
 // to keep some browsers from misinterpreting JSON output as HTML.
 //
-// Array and slice values encode as JSON arrays, except that
-// []byte encodes as a base64-encoded string, and a nil slice
+// Array and slice values encode as JSON arrays, except that a slice of
+// bytes encodes as a base64-encoded string, and a nil slice
 // encodes as the null JSON object.
 //
 // Struct values encode as JSON objects. Each exported struct field

この変更は機能的なものではなく、ドキュメンテーションの改善です。

  • 以前のコメントは「[]byte はBase64エンコードされた文字列としてエンコードされる」と述べていました。
  • 新しいコメントは「バイトのスライス(a slice of bytes)はBase64エンコードされた文字列としてエンコードされる」と、より正確な表現に修正されています。
  • これは、[]byte がGoの「スライス」であり「配列」ではないことを明確にし、読者が []byte の挙動をより正確に理解できるようにするための微調整です。

これらの変更により、encoding/json パッケージは []byte 型とそのエイリアス型をより堅牢かつ一貫して処理できるようになり、開発者がJSONとGoのデータ構造を扱う際の予期せぬ挙動が減少しました。

関連リンク

参考にした情報源リンク