pk-n
pk-n

Reputation: 576

to_json on ActiveRecord object ruby 3.0

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

Answers (3)

Alexander Orlov
Alexander Orlov

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.

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

honey
honey

Reputation: 1077

  {'user': User.first }.as_json

Use as_json instead of to_json

Upvotes: 0

Sebasti&#225;n Palma
Sebasti&#225;n Palma

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

Related Questions