emahiro/b/logs

勉強記録と書評とたまに長めの呟きを書きます

json.Unmarshalではnilの参照型へのmappingはできない

httpレスポンスを構造体にmappingする際に以下のようなコードを書くケースは多々あると思います。 ※ error handlingは割愛

// responseの取得
resp, _ := http.Get(url)
body := resp.Body
defer body.Close()

// []byteへの変換
b, _ := ioutil.ReadAll(body)

var data *Data

// Data structへのjsonのmapping
if err := json.Unmarshal(b, data); err != nil{
    panic(err)
}

しかしこのコードは一見正しいように見えますが、期待する動作はしません。
以下のようなerrorが発生します。

json: Unmarshal(nil *model.Data)

参照型でもnilの構造体にはUnmarshal(復元)して構造体へのmappingはできないというエラーです。

実は json.Unmarshal(src, dist) において dist にとるべきinterface型の変数はnilではいけません。
nilではなく空の参照型の構造体を指定する必要があります。
そのため、上記のコードは以下のように書き換える必要があります。

// responseの取得
resp, _ := http.Get(url)
body := resp.Body
defer body.Close()

// []byteへの変換
b, _ := ioutil.ReadAll(body)

// ここで空の構造体を指定する。
data := &Data{}

// Data structへのjsonのmapping
if err := json.Unmarshal(b, data); err != nil{
    panic(err)
}

data := &Data{} とすることで json.Unmarshal でstrcutにmapping可能になります。

なぜこのような挙動になるかを調べました。

json.Unmarshalの公式ドキュメントによると

Unmarshal parses the JSON-encoded data and stores the result in the value pointed to by v. If v is nil or not a pointer, Unmarshal returns an InvalidUnmarshalError.

と冒頭に記述されており、 If v is nil or not a pointer, Unmarshal returns an InvalidUnmarshalError にある通り、「nilの参照型」or 「参照型でない」場合はErrorを返すことが公式に書かれています。

つまり、答えはまんま「nilの参照型は取れない」ということで完結します。

json.Unmarshal(src, dist) において、nilの参照型を取れないことについては以下のstackoverflowも参考になると思います。

Why does json.Unmarshal work with reference but not pointer?

エントリーの中で記述されている

To unmarshal JSON into a pointer, Unmarshal first handles the case of the JSON being the JSON literal null. In that case, Unmarshal sets the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into the value pointed at by the pointer. If the pointer is nil, Unmarshal allocates a new value for it to point to.

の中で、実際にUnmarshalメソッドがjsonを参照型に復元するときの挙動について記載してあって、これによると、unmarshalメソッドは復元プロセスの中で参照型をnilにする、しかし、復元対象の格納先の参照型の変数がすでにnilの場合、nilnilにするという挙動が発生し、そのためにerrorが発生するのだという解釈をしました。