Reputation: 576
I am using rails 6.0.3.6 and ruby 3.0.0,
When I call {'user' : User.first }.to_json
I am getting "{\"user\":\"#<User:0x00007fa0a8dae3c8>\"}"
same with [User.first, User.last].to_json
If I switch back to ruby 2.7.2,
I get proper result ie <User:0x00007fa0a8dae3c8>
replaced with all it's attributes.
Any idea what I am missing?
Upvotes: 5
Views: 966
Reputation: 1
Managed to consistently reproduce this on:
Rails 6.0.3.5
Ruby 3.0.5
Fun fact: Ruby 2.7.7 - fixes the issue. As well does upgrading to Rails 6.1+. So appears strictly with such set of versions.
Tried to touch https://rubygems.org/gems/json versions. Does not affect
Tried to touch https://rubygems.org/gems/jbuilder versions. Does not affect
Let's get straight to business.
To consistently reproduce and understand that you have the problem, open Rails console and test the next:
[User.first, User.last].to_json
=>
"[\"#<User:0x00007fbb497fad30>\",\"#<User:0x00007fbb5001df20>\"]"
{ test: User.limit(2).to_json }.to_json
=>
"{\"test\":\"#<User::ActiveRecord_Relation:0x00007fc0228b7d08>\"}"
If you see such output - unluckily you have this problem, but that's not a tragedy ;)
Issues that I met in field-tested conditions, and somebody could find them typical for their project & useful for reproducing/debugging/testing process:
if you have any API endpoints with render json: { user: user }
syntax
jbuilder views, which use the next definition json.(@user, :id, :profile)
or some non-serialized ActiveRecord object e.g. json.profile @user.profile
all scenarios, when you try to explicitly convert to json any Object inside Enumerable classes (you can grep-search all .to_json usages)
payload = [
<ActionController::Parameters {"user"=>{"profile"=>1}} permitted: false>
1,
2
].to_json
JSON.parse(payload)
=>
[
String (instead of Hash),
1,
2
]
Solution:
# config/initializers/monkey_patches.rb
# TODO: Remove after upgrade to Rails 6.1+. JSON serialization of Objects works fine there
# Source: https://stackoverflow.com/questions/66871265/to-json-on-activerecord-object-ruby-3-0
module ActiveSupport
module FixBrokenJsonSerialization
def to_json(options = nil)
return ::ActiveSupport::JSON.encode(self) if options.is_a?(::JSON::Ext::Generator::State)
super(options)
end
end
end
Object.prepend(ActiveSupport::FixBrokenJsonSerialization)
NB. It's not a silver bullet, it's a monkey-patch, what is always risky and bad. Better solution would be to migrate to Rails 6.1 or degrade to Ruby 2.7.7 (the last one is actually pretty annoying).
In case you need exactly these versions - Please, also, make sure that you don't have any libraries with heavy JSON logic or relying in any way to JSON::Ext::Generator::State
.
Other than that fact - should help to repair most of places at once 👍
Upvotes: 0
Reputation: 33491
The problem is in Rails 6.0.3.6 when invoking to_json
on {'user' : User.first }
Rails end up adding a JSON::Ext::Generator::State
argument for to_json
, so options.is_a?(::JSON::State)
returns true and super(options)
is returned.
From the definition of to_json
:
def to_json(options = nil)
if options.is_a?(::JSON::State)
# Called from JSON.{generate,dump}, forward it to JSON gem's to_json
super(options)
else
# to_json is being invoked directly, use ActiveSupport's encoder
ActiveSupport::JSON.encode(self, options)
end
end
While in more recent of Rails to_json
is invoked without any argument and the branch takes the path to finally return ActiveSupport::JSON.encode(self, options)
.
So, in your case you could do
{ 'user': User.first.attributes }.to_json
To bypass the problem.
Upvotes: 4