iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article

Hashes that get JSON.parsed instead of JSON.generated

に公開

A story about Ruby.

In Ruby, there is a method called JSON(object) that returns a Hash if you pass it a string, and converts it to JSON if you pass it a Hash, as shown below.

h = {a: 1}
JSON(h) #=> "{\"a\": 1}"

j = "{\"a\": 1}"
JSON(j) #=> {a: 1}

However, this doesn't work for certain "ill-behaved" Hashes extended via method_missing. For example, this is the case with Rails encrypted credentials in Rails 7.

h = Rails.credentials.hoge
h.to_a? Hash #=> true
JSON(h) #=> TypeError: no implicit conversion of nil into String

Looking at the implementation of JSON(object), it is as follows. If object.respond_to? :to_str is true, it is treated as a string and JSON.parse is executed; otherwise, JSON.generate is executed.

def JSON(object, *args)
  if object.respond_to? :to_str
    JSON.parse(object.to_str, args.first)
  else
    JSON.generate(object, args.first)
  end
end

In the previous example, h.respond_to? :to_str becomes true. This is because Rails credentials are set up to return something for all calls (including to_str) via method_missing. Therefore, it behaves as follows:

h = Rails.credentials.hoge
h.respond_to? :to_str #=> true
h.to_str #=> nil

As a result, within JSON(h), it is treated as a string-like object and JSON.parse is executed, resulting in an error.

While I feel this might be more of an issue with ActiveSupport than the JSON implementation, I thought it would be more rigorous to check not just respond_to? :to_str but also whether the result of to_str is actually a String to verify it is a string-like object, so I submitted a PR. Well, it's perfectly fine if it's not merged. After all, using an explicit JSON.generate would solve the problem anyway...
https://github.com/flori/json/pull/499

Discussion