Help us understand the problem. What is going on with this article?

Go言語のJSON/Unmarshalはcase insensitiveに変数を展開するが名前がカブった時どうなるか?

More than 1 year has passed since last update.

簡単に、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. 普通に変換(JSONとGo言語側でキーの大文字小文字が合っている)
  2. 普通に変換(JSONとGo言語側でキーは一致するが大文字小文字が違う)

これは1も2も変数に正しく値が設定される。想定通り。

非正常系

異常系というか、Case insensitiveに名前を重複させるとか、JSON上キーを重複させるとか、普通はやらなそうなことをする。

  1. JSONのキーが重複している(Case sensitive)。Go側の変数は1つ。
  2. JSONのキーが重複している(Case insensitive)。Go側の変数は1つ。
  3. JSONのキーが重複している(Case sensitive)。Go側の変数は複数(Case sensitive)。
  4. JSONのキーが重複している(Case insensitive)。Go側の変数は複数(Case sensitive)。
  5. JSONのキーは1つ。Goの変数は複数(Case sensitive)。

実験の結果

  1. JSON側の最後のキーの値が設定された
  2. 大文字小文字を無視してJSON側の最後のキーの値が設定された
  3. Case sensitiveに一致する名前の変数にそれぞれ値が設定された
  4. Case sensitiveに一致する変数にのみ値が設定された
  5. 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がないので、複数の変数に値が入りそうな気がするのだが…。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした