Try Golang! JSONデコード時のキーの一致判定
How to match keys when decoding JSON
先日、こわーい先輩からこんな指摘を受けました。
APIの仕様書ではJSONのキーが “id” になっているけど、 “Id” で呼び出して動いたんだけど、どうなってんの?ちゃんとしてよ!
あれ?JSONタグミスったかな?と思いながら調べてみると、ちょっと面白い発見があったので、今日はそのお話です。(ちなみにJSONタグはミスっていませんでした)
なお、本事象は、執筆時点の最新である go1.14.3
で確認しています。
GoでJSONを扱う時のおさらい
GoでJSONをデコードする場合は、マップを利用することもできますが、次のような感じで構造体を定義しておくことが多いのかなと思います。
type foo strcut {
ID string `json:"id"`
Value string `json:"value"`
}
`json:"id"`
といった記述は、JSON形式に変換する場合のキー名を指定するJSONタグです。大文字小文字、スネークケースなど、Goのフィールド名に依存させたくない場合に使用します。
このように構造体を定義しておけば、次のようなJSONをデコードすることができます。
{"id":"xxx","value":"yyy"}
実際の挙動はPlaygroundで確認できます。
本当に“id”ではなくて”Id”で読み込めてしまうのか
では、本題に戻りましょう。Playground上で、JSONのキーを次のように変更しても、確かに問題なくデコードできていることが分かります。
{"Id":"xxx","Value":"yyy"}
実は Id
だけでなく、 ID
も iD
も、全て構造体の ID
フィールドにマッピングされます。どうやら大文字小文字は気にしていないようです。
では試しに、大文字小文字をごちゃ混ぜにした、次のようなJSONを読み込ませてみましょう。
{"id":"a","ID":"b","Id":"c","iD":"d","vAlUe":"yyy"}
結果は下記のようになります。どうやら同じフィールドにマッピングされるキーが複数存在した場合は、後勝ちになるようです。
{ID:d Value:yyy}
標準パッケージを読み解いてみる
Goの標準パッケージ encoding/json
を読み進めていくと、該当のロジックは下記にありました。
「JSONのキー名のマップから取得ができなかった場合に、大文字小文字を無視してマッピングしますよ」とコメントにも記載されています。さらに、Godocにも下記のように記載されています。
…, preferring an exact match but also accepting a case-insensitive match.
「完全一致を優先し」てはいないような気もしますが、ひとまず今回発生した事象についてはこれで説明ができました。
今の挙動は今後変更されるかもしれない
実はGitHubに、今回の事象についてのIssueがあります。やはり皆さん、今の挙動に納得していない様子。デコーダーに UseStrictNames
メソッドを追加して大文字小文字を無視しないオプションを用意する案や、JSONタグでフィールド毎に個別に制御する案が上がっていますが、まだどのように対応されるかよく分からない感じです。
ただ、「完全一致のフィールドが見つかった後は、不完全な一致(大文字小文字を無視した一致)のデコードをスキップする」という修正がChange Listにあがっているため、単純な後勝ちになる今の挙動は、もしかしたら変更されるかもしれません。
大文字小文字を無視するとキーが重複してしまうようなJSONを使用することはなかなかないと思うので、今の挙動が問題となることは少ないように思います。ただ、今の仕様を頭の片隅に置いておいてあげると、こわーい先輩に指摘されても、冷静に回答できるかもしれませんね。