簡単に、Go言語のJSON to struct変換の話
Goは標準APIでJSON文字列と構造体を相互変換できる。
標準APIでは、Go→JSON文字列の変換をMarshal、JSON文字列→Go世界の変換をUnmarshalと呼ぶ。
API Referenceやサンプルのとおり、JSON文字列→構造体にunmarshalする際、JSON側のオブジェクトのキーと、Go側のstructのメンバー変数の変数名が一致していれば、その変数に値が代入される。
そして、リファレンスに記載されているとおり、キーと名前の一致についてはCase Insensitiveで比較する。
この実装が正しいかどうかはRFC7159にも書いておらず(JSON自体の定義なのでそれはそう)、Case Sensitiveな実装もあるし、例えばJSON-RPCだと仕様上case-sensitiveだそう。
疑問
ここから先は、現実的には基本的にそんな使い方はしないと思われるため、あくまで実験です。
Go言語はソースコード的にはcase sensitiveだ。
structのほうはCase sensitiveなのに、unmarshalがCase insensitiveだと、キーが重複するとどの変数に値が入るのかリファレンスからわからないが、一体どうなるのか。
検証のため以下のような実験してみる。
実験
正常系
とりあえず普通の使い方をする。
これは1も2も変数に正しく値が設定される。想定通り。
非正常系
異常系というか、Case insensitiveに名前を重複させるとか、JSON上キーを重複させるとか、普通はやらなそうなことをする。
- JSONのキーが重複している(Case sensitive)。Go側の変数は1つ。
- JSONのキーが重複している(Case insensitive)。Go側の変数は1つ。
- JSONのキーが重複している(Case sensitive)。Go側の変数は複数(Case sensitive)。
- JSONのキーが重複している(Case insensitive)。Go側の変数は複数(Case sensitive)。
- JSONのキーは1つ。Goの変数は複数(Case sensitive)。
実験の結果
- JSON側の最後のキーの値が設定された
- 大文字小文字を無視してJSON側の最後のキーの値が設定された
- Case sensitiveに一致する名前の変数にそれぞれ値が設定された
- Case sensitiveに一致する変数にのみ値が設定された
- Case sensitiveに一致する変数にのみ値が設定された
なおキーや変数名はコード上順番を変えても同じ結果が得られた。
新たな疑問と推測
リファレンス上の定義どおり自分が実装するなら単純に、JSON側は後勝ち、struct側は先勝ちにする気がする。
つまり
{"Name": "Cat", "NAME": "Dog"}
を入力として
type Animal struct {
NAME string
Name string
}
を出力とする場合、以下の結果となるように実装すると思う。
- NAME: "Cat"
- Name: ""(空文字=null値)
前提としてJSONは頭からストリームとして処理するし、structのメンバー変数をリフレクションで取得した時コード上の定義順に取得できるものとする。
JSON側はとにかく上から処理し、Goのstructからリフレクションで変数名を取得し、大文字小文字を無視して最初に見つかった変数に値をセットして処理を抜ける。
しかし実際には下記の状態になる。
* NAME:Dog
* Name:Cat
この結果だけだとGo言語のencode/json.Unmarshal
はcase sensitiveな動作をしている用に見える。
推測するに、まずcase sensitiveな比較を行って、一致するキーがなかった場合にcase insensitiveな比較をしているようだ。
実装を読む
当該箇所はencoding/jsonのdecode.go 697行目付近
Mapとstructのバインディングが同じ関数で処理されているので(再帰的に処理する場合を考慮している?)だいぶ長い関数だが、擬似コードでいうと以下の処理になっている。
for j := range json側のフィールドリスト {
for k := range struct側のフィールドリスト {
if jsonのフィールド名とstructのフィールド名がバイナリ的に同一なら {
structにjsonの値をセット
break
}
if structのフィールドがまだ空 && jsonのフィールド名とstructのフィールド名がcase-insensitiveに同一なら { // 実際はUnicodeにCase folding的に同等なら
structにjsonの値をセット
}
}
}
ということで、case-sensitive優先で処理しているようだ。
しかしフェイルバックのほうにbreakがないので、複数の変数に値が入りそうな気がするのだが…。