ゲームっぽい何かを作りながらRubyを勉強する　その３　〜JSON〜

今回は[mygame]()リポジトリに何か実装を追加した、という話ではないのですが、調査としてRubyにおけるJSONの扱いについて調べてみましたので、わかったことをメモします。

# 見たもの

* [JSONモジュール - Ruby](https://docs.ruby-lang.org/ja/latest/class/JSON.html)

とりあえず日本語のリファレンスマニュアルは一通り読んでみました。
`load`で文字列 -> オブジェクトの変換、`dump`でオブジェクトからJSON文字列への変換とのこと。ふむふむ。

* [json/common.rb](https://github.com/ruby/ruby/blob/202bbda2bf5f25343e286099140fb9282880ecba/ext/json/lib/json/common.rb)

ついでにRubyで提供されているAPIはソースコードが割と簡単に読めるということを`pry`してたら気づいたので（エラーにファイルパスが書いてあった）、読んでみました。GitHubにも上がっていますので、Web上で見たい場合ば上記のリンクから。バージョンの違いだけ注意です。

# 分かったこと

## `load`に渡すオブジェクトは文字列以外もいける

リファレンスマニュアルには

> JSON 形式の文字列を指定します。他には、to_str, to_io, read メソッドを持つオブジェクトも指定可能です。

と書いてあり、実際ソースコードを読んでも

```rb:common.rb
def load(source, proc = nil, options = {})
    opts = load_default_options.merge options
    if source.respond_to? :to_str
      source = source.to_str
    elsif source.respond_to? :to_io
      source = source.to_io.read
    elsif source.respond_to?(:read)
      source = source.read
    end
    if opts[:allow_blank] && (source.nil? || source.empty?)
      source = 'null'
    end
    result = parse(source, opts)
    recurse_proc(result, &proc) if proc
    result
end
```

と書いてあり、最初に`source`が`to_str`, `to_io`, `read`メソッドを持っているかを（左の順番で）調べ、あればそれを使ってJSON文字列を得ようとしている。

つまり、`source`として与えるのは文字列だけでなく`Pathname.pwd.join('hoge.txt')`などで得られたファイルオブジェクトをそのまま渡しても問題ない、ということになる。

```rb:pry
pry(main)> require 'pathname'
pry(main)> require 'json'

pry(main)> JSON.load(Pathname.pwd.join('sample_json.txt'))
=> {"hoge"=>111}
```

## dumpするときのネストの深さはデフォルトで~~19まで~~無限

`unparse`メソッドの説明に、

> __:max_nesting__
入れ子になっているデータの最大の深さを指定します。 偽を指定すると深さのチェックを行いません。デフォルトは 19 です。

と書いてある通り、デフォルトでは入れ子のデータの最大の深さはデフォルトでは19になっている。これは`unparse`の説明だが、`dump`の場合も中で`JSON.#generate`を呼び出していて、そこには`unparse`と同じ説明が書いてあるので、 ~~やはりデフォルト上限は19になっている。~~

（2016/11/30 追記）
違いました。
以下に引用してるソースコードにも書いてありますが、指定がない場合は`JSON.dump_default_options`を使っていて、その定義は以下のようになっています。

```rb:common.rb
self.dump_default_options = {
  :max_nesting => false,
  :allow_nan   => true,
}
```

というわけで、`dump`メソッドを使った場合のデフォルトの`max_nesting`は`false`つまり「チェック無し」でした。
（/追記）

この制限を取り外したい場合は

```rb: pry
pry(main)> JSON.dump({:hoge => 111}, limit = 100)=> {"hoge"=>111}
```

というように引数で`limit`を指定する。
と、`dump`メソッドは中で、

```rb:common.rb
def dump(obj, anIO = nil, limit = nil)

...省略

  opts = JSON.dump_default_options
  opts = opts.merge(:max_nesting => limit) if limit
  result = generate(obj, opts)

...省略

end
```

というように与えられた`limit`を`generate`の`:max_nesting`オプションとしてそのまま渡している。
おそらく`limit`に数値ではなく`false`を与えれば、上記の引用部分に書かれている通り入れ子のチェック自体が行われなくなる。と思う。

## 循環参照に注意

> __:check_circular__
真を指定した場合、生成するオブジェクトの循環をチェックします。 この動作がデフォルトです。

というオプションがある通り、`dump`する際は与えるオブジェクトの循環参照に注意する。

これはRubyに限った話ではないような気もするが、Javaではクラス定義だけ見ていれば循環参照が起こり得るかすぐに分かる上、そもそもデータを表すクラスで循環参照しようと思ったこともなかったので今まで意識していなかったのですが、改めてなるほどと思ったのでメモ。

ちなみに循環参照が起こりえない自信がある場合はチェックそのものを行わない（つまり `{:check_circular => false}`が指定された)`fast_generate`というメソッドもある。`fast`というくらいなのでたぶん`dump`よりも早い。でも`limit`などは指定できない。

と思って念のため実装みたら、第２引数に`opts`を渡せば他のオプションについても指定可能な様子。

```rb:common.rb
def fast_generate(obj, opts = nil)
  if State === opts
    state, opts = opts, nil
  else
    state = FAST_STATE_PROTOTYPE.dup
  end
  if opts
    if opts.respond_to? :to_hash
      opts = opts.to_hash
     elsif opts.respond_to? :to_h
      opts = opts.to_h
    else
      raise TypeError, "can't convert #{opts.class} into Hash"
    end
    state.configure(opts)
  end
  state.generate(obj)
end
```

理解が間違っていなければ、「`opts`が`State`でなければ、'FAST_STATE_PROTOTYPE'（`:max_nesting => false`が指定されたSTATE)をベースに、`opts`の設定を追加する」という実装になっているので、他のオプションについても上書きで指定が可能なようです。

実装見ないとメソッドにどんな引数を渡せば良いかも分からない部分があるのが、Javaに慣れている身としては慣れないところです。

（と思ったら[rdoc](https://docs.ruby-lang.org/en/2.3.0/JSON.html#method-i-fast_generate)にはちゃんと書いてありました。うむむ。。。）

# まとめ

というわけで、RubyでJSONを扱う際の基本を調べてみました。
ゲームっぽい何かのセーブデータなどを扱う上で使うような気がしたので軽く調べてみましたが、意外とドキュメントやソースコード、`pry`での動作確認などを使えば調べるのは簡単で、分かったことも多かった印象です。

まだまだ「Rubyでこれやる場合はどう書けばいいんだろう」の疑問は数多くありますので（DBアクセスとかHTTP通信とか）、引き続き調べつつ実際に使いつつ、Ruby自体に慣れていければと思います。