今回はmygameリポジトリに何か実装を追加した、という話ではないのですが、調査としてRubyにおけるJSONの扱いについて調べてみましたので、わかったことをメモします。
見たもの
とりあえず日本語のリファレンスマニュアルは一通り読んでみました。
load
で文字列 -> オブジェクトの変換、dump
でオブジェクトからJSON文字列への変換とのこと。ふむふむ。
ついでにRubyで提供されているAPIはソースコードが割と簡単に読めるということをpry
してたら気づいたので(エラーにファイルパスが書いてあった)、読んでみました。GitHubにも上がっていますので、Web上で見たい場合ば上記のリンクから。バージョンの違いだけ注意です。
分かったこと
load
に渡すオブジェクトは文字列以外もいける
リファレンスマニュアルには
JSON 形式の文字列を指定します。他には、to_str, to_io, read メソッドを持つオブジェクトも指定可能です。
と書いてあり、実際ソースコードを読んでも
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')
などで得られたファイルオブジェクトをそのまま渡しても問題ない、ということになる。
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
を使っていて、その定義は以下のようになっています。
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
メソッドは中で、
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
などは指定できない。
と思って念のため実装みたら、第2引数にopts
を渡せば他のオプションについても指定可能な様子。
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にはちゃんと書いてありました。うむむ。。。)
まとめ
というわけで、RubyでJSONを扱う際の基本を調べてみました。
ゲームっぽい何かのセーブデータなどを扱う上で使うような気がしたので軽く調べてみましたが、意外とドキュメントやソースコード、pry
での動作確認などを使えば調べるのは簡単で、分かったことも多かった印象です。
まだまだ「Rubyでこれやる場合はどう書けばいいんだろう」の疑問は数多くありますので(DBアクセスとかHTTP通信とか)、引き続き調べつつ実際に使いつつ、Ruby自体に慣れていければと思います。