JSON.generateしてほしいのにJSON.parseされてしまうHash
■JSON(FIREBASE) がRails7から TypeError: no implicit conversion of nil into String from /usr/local/lib/ruby/3.1.0/json/common.rb:216:in initializeというエラーを吐くようになった理由
FIREBASE 変数の実体はクラス初期化時に Rails.application.credentials.firebase で初期化されたものである。つまり FIREBASE の中身は
JSON(Rails.application.credentials.firebase) なわけであるが、そもそも JSON(object) という呼び出し方はどういう挙動をするのか。
ソースコードとしてはここ(flori/jsonはrubyの標準ライブラリのjsonの実装と同じ)。
code:rb
def [](object, opts = {})
if object.respond_to? :to_str
JSON.parse(object.to_str, opts)
else
JSON.generate(object, opts)
end
end
JSON(object)の動きとしてはobjectが :to_str出来ればJSON文字列と見做しHashへ変換する JSON.parseを呼び出し、それ以外であればObjectをJSONにシリアライズする JSON.generateを呼び出す。つまり object.respond_to?にどう反応するかで真逆の挙動を行うわけである。
ところでRails.application.credentials.firebase はどういうオブジェクトかというとRails7より前では Hashであるのに対してRails7からは ActiveSupport::InheritableOptions というオブジェクトになっている。ActiveSupport::InheritableOptionsとは何かというと、一言で言うと Hash に対するアクセスを hash:hoge だけでなく hash.hogeというアクセスを可能にするためのオブジェクトだ。 ではこのActiveSupport::InheritableOptions がどのように実装されているかというとこんな具合である。重要なのは下記である。
code:rb
def method_missing(name, *args)
name_string = +name.to_s
if name_string.chomp!("=")
else
bangs = name_string.chomp!("!")
if bangs
selfname_string.presence || raise(KeyError.new(":#{name_string} is blank")) else
end
end
end
method_missingだ。method_missingは雑に説明するとそのオブジェクトに実装されてないメソッド呼び出しが起きた時に呼ばれるメソッドである。
Rails.application.credentials.firebase.methodsとやるとRails7では :method_missingが追加されている。これによって何が起きるのか?下記のようになる。
code:rb
# Rails6の場合
Rails.application.credentials.firebase.to_str
NoMethodError: undefined method `to_str' for {}:Hash
# Rails7の場合
Rails.application.credentials.firebase.to_str
nil
Rails6ではNoMethodErrorになる、一方Rails7ではNoMethodErrorではなく(メソッドの定義が無いのにエラーではなく)nilが返る。これは method_missingによる影響だ。 ということは、 respond_to?に対してはどう動くかというとこのようになる。
code:rb
# Rails6の場合
Rails.application.credentials.firebase.respond_to? :to_str
false
# Rails7の場合
Rails.application.credentials.firebase.respond_to? :to_str
true
ということでRails7では Rails.application.credentials.firebase に対して :to_strメソッドが定義されていないのに定義されていることになってしまっている。よって先の JSON(object) の挙動としては、文字列ではないオブジェクト(正確にはto_strできないオブジェクト)であるにも関わらず JSON.parse(object.to_str, opts) が実行されてしまうことになった。
code:rb
def [](object, opts = {})
if object.respond_to? :to_str
JSON.parse(object.to_str, opts)
else
JSON.generate(object, opts)
end
end
Rails.application.credentials.firebase.to_str は先のコードで実験した通り nil になるので JSON.parse(nil) が実行される。これは TypeError: no implicit conversion of nil into String from /usr/local/lib/ruby/3.1.0/json/common.rb:216:in initialize というエラーを発生させる。
表題のエラーである。
なんてこったい…