JSON.parseのquirks_modeと2.4.0での変更
どんだけJSONネタで引っ張るのかという話もあるが、色々と調べて見て分かった事があったのでブログに記録しておく。
JSON.parseでは:quirks_modeというオプションがある。
通常JSONを解析する場合、そのトップレベルはオブジェクト若しくは配列になっている必要があるはずだが、 このオプションが真のときは単一のパラメータを解釈してくれる。
irb(main):001:0>require 'json' => true irb(main):002:0>JSON.parse("12",{:quirks_mode=>true}) => 12 irb(main):003:0>JSON.parse("12",{:quirks_mode=>false}) JSON::ParserError: 784: unexpected token at '12' from /Users/nekomaho/.rbenv/versions/2.3.1/lib/ruby/2.3.0/json/common.rb:156:in `parse' from /Users/nekomaho/.rbenv/versions/2.3.1/lib/ruby/2.3.0/json/common.rb:156:in `parse' from (irb):9 from /Users/nekomaho/.rbenv/versions/2.3.1/bin/irb:11:in `<main>' irb(main):004:0> JSON.load(nil,nil,{:quirks_mode=>true}) => nil irb(main):005:0> JSON.load(nil,nil,{:quirks_mode=>false}) TypeError: no implicit conversion of nil into String from /Users/nekomaho/.rbenv/versions/2.3.1/lib/ruby/2.3.0/json/common.rb:156:in `initialize' from /Users/nekomaho/.rbenv/versions/2.3.1/lib/ruby/2.3.0/json/common.rb:156:in `new' from /Users/nekomaho/.rbenv/versions/2.3.1/lib/ruby/2.3.0/json/common.rb:156:in `parse' from /Users/nekomaho/.rbenv/versions/2.3.1/lib/ruby/2.3.0/json/common.rb:335:in `load' from (irb):4 from /Users/nekomaho/.rbenv/versions/2.3.1/bin/irb:11:in `<main>'
このオプション、ruby 2.4.0から廃止され:allow_blankというオプションになっている。
正確には、nil以外の単一のパラメータを解釈するのがデフォルトの挙動となり、 :allow_blankで"nil"を受けるかどうかのみ変更出来るようになったというのが正しい。
irb(main):001:0>require 'json' => true irb(main):002:0> JSON.load(nil,nil,{:allow_blank=>true}) => nil irb(main):003:0> JSON.load(nil,nil,{:allow_blank=>false}) TypeError: no implicit conversion of nil into String from /Users/nekomaho/.rbenv/versions/2.4.0/lib/ruby/2.4.0/json/common.rb:156:in `initialize' from /Users/nekomaho/.rbenv/versions/2.4.0/lib/ruby/2.4.0/json/common.rb:156:in `new' from /Users/nekomaho/.rbenv/versions/2.4.0/lib/ruby/2.4.0/json/common.rb:156:in `parse' from /Users/nekomaho/.rbenv/versions/2.4.0/lib/ruby/2.4.0/json/common.rb:335:in `load' from (irb):15 from /Users/nekomaho/.rbenv/versions/2.4.0/bin/irb:11:in `<main>'
という指摘も出ていて、本当にこの変更がそのまま2.4系に適用されるのかは怪しい。
関連
JSON.loadはオプションが取れる
先日の記事の中でJSON.loadのgithubの実装を紹介した。
Rubyの公式ドキュメント(以下るりま)JSON.loadの説明とgithubのコードを眺めていてあれ?るりまの説明と実装が違う。。。と言う事に気がついた。
るりま側のloadの定義は以下の通り
load(source, proc = nil) -> object
github側のloadの定義は以下の通りで、最後にオプションを取るかどうかがるりま側の物と異なる。
def load(source, proc = nil, options = {})
実際に試してみると、procの後ろにparseで指定出来るオプションをハッシュで渡すとちゃんと動く。
hoge1.json
{"color":{"red":1,"blue":2,"green":NaN}}
hoge.rb
require 'json' File.open("hoge1.json") do |f| p JSON.load(f,nil,{ :symbolize_names=>true }) end => {:color=>{:red=>1, :blue=>2, :green=>NaN}}
これ、今まで「loadはoptionが渡せないからfileをreadしてからparseを使おう」と考えていた人も結構いるだろうなと思う。
なお、もっというとJSON.loadはload_default_optionsというインスタンス変数をメソッド内でデフォルトオプションとして定義していて、 下記のようなオプションが事前に設定されている。
require 'json' p JSON.load_default_options => {:max_nesting=>false, :allow_nan=>true, :quirks_mode=>true, :create_additions=>true}
このload_default_optionsはatter_accessorとして定義されており、 外から設定を変更する事が出来る。
要するに、loadにオプションを指定する方法は、loadの第3引数に値を与える方法と、 load_default_optionsに値をセットする方法の2通りがあるという事になる。
自分は前者は一時的に指定するオプションを、 後者はアプリケーション全体で指定するオプションを設定するようなイメージかと勝手に解釈してる。
ところでこのJSON.#load、ソース上に以下の記述がある。
# BEWARE: This method is meant to serialise data from trusted user input, # like from your own database server or clients under your control, it could # be dangerous to allow untrusted users to pass JSON sources into it. The # default options for the parser can be changed via the load_default_options # method.
詰まる所、このメソッドは与えられるJSONが必ず信頼出来る物でなければいけないという制約があるようで、 実装上はparseの方を使う方が安全なようだ*1
上記の制約は恐らく :create_additionsがデフォルトでtrueな事が原因だと思うが、それなら今回の方式でfalseに出来そうな気もする。正直よく分からないので、知っている人いたら教えて下さい。。。
とりあえず、loadの記載については以下の記事参考にして、るりまに修正のプルリクエストを 投げてみた
http://qiita.com/tbpgr/items/2072a9a743918b8cfb34
※何か更新しない理由があって、直さなくていい物だったら恥ずかしいけど。。。これも勉強ですな。
※2017/2/11追記 mergeして頂きました。
JSON.parseとJSON.loadの違い
JSON.parseとJSON.loadの差が何なのか分からなかったので調べてみた。
※下記の参考のStackOverflowの内容がその答えだが、裏を取りたかった。
require 'json' File.open("hoge.json") do |f| JSON.parse(f.read) == File.open("hoge.json"){ |f| JSON.load(f) } ? p("same") : p("diffrent") end => "same"
※File.openを2回呼んでいるのはどげんかせんといかん。
Rubyの公式ドキュメント:parse、:loadを見ると、parseはparamのsourceがJSONの文字列しか受け付けないのに対して、 loadはto_str,to_io, readを持つオブジェクトもparamのsourceに指定可能と記載されている。
githubのJSON.loadの実装を見ると、loadがparseをラップしているのが分かる。
今回のようにファイルから読み込む場合、parseはreadから返された文字列が、loadはFile.to_io.readで返された文字列が使われる為、両方供同じようにfileからJSONファイルを読み込める形になるようだ。
loadはドキュメントの通り、loadに渡されたオブジェクトがto_str,to_io,readのどれかを返せばいいので、 String以外のオブジェクトからparseを呼び出す際は、loadを使用する形になるのではないかと思う。
参考
StackOverflow:What’s the difference between JSON.load and JSON.parse methods of Ruby lib?
classとそれが読み込んでいるmoduleの一覧を表示する方法
クラスの継承関係と各クラスで読み込んでいるモジュールの一覧を表示したかったので作ってみた。
def display_class_modules(class_name) print "class:#{class_name},modules:#{class_name.included_modules}","\n" if class_name.respond_to?(:included_modules) display_class_modules(class_name.superclass) if class_name.respond_to?(:superclass) end display_class_modules(1.class) #=> class:Fixnum,modules:[Comparable, Kernel] #=> class:Integer,modules:[Comparable, Kernel] #=> class:Numeric,modules:[Comparable, Kernel] #=> class:Object,modules:[Kernel] #=> class:BasicObject,modules:[]
1のクラスはFixnumでComparableとKernelモジュールをインクルードしている事と、 継承チェーンがFixnum->Integer->Numeric->Object->BasicObjectの順に上がっている事が分かる。
疑問点
- Moduleは何処に行ったのだろう?(included_modulesにclass_nameが反応しているのにModule自体がイングルードしているモジュール一覧に出てこない)
改善ポイント
- moduleの中のinclude関係も見れるとよい
- もっと簡潔に書きたい(respond_to?とかまとめて書けないか)