oj icon indicating copy to clipboard operation
oj copied to clipboard

Oj.generate: don't call as_json on rails model

Open yosiat opened this issue 2 years ago • 8 comments

Hi,

Since I upgraded to Oj 3.11.4 (from 3.11.2) json serialization of rails models don't work as expected.

# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  # Activate the gem you are reporting the issue against.
  gem "activerecord", "~> 6.1.0"
  gem "sqlite3"
  gem "oj", "3.11.4"
end

require "active_record"
require "minitest/autorun"
require "logger"
require "oj"

Oj.optimize_rails

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")

ActiveRecord::Schema.define do
  create_table :posts, force: true do |t|
    t.string :title
    t.string :body
  end
end

class Post < ActiveRecord::Base
end

post = Post.create!(title: "Hello", body: "World")

puts Oj.generate({ post: post })

With Oj 3.11.2, the output is:

{"post":{"id":1,"title":"Hello","body":"World"}}

And with 3.11.4, the output is:

{"post":"#<Post:0x00007f808b8b30b8>"}

Am I missing configuration step ? or is it a bug?

yosiat avatar Sep 22 '21 13:09 yosiat

Have you tried the most recent Oj? 3.11.4 is 5 months out of date.

ohler55 avatar Sep 22 '21 13:09 ohler55

@ohler55 same with 3.13.7, I upgraded from 3.11.2 and did manual bisect to find the version who caused this bug.

yosiat avatar Sep 22 '21 13:09 yosiat

I see, okay will try to identify what changed and if it is different than what active support does. Thanks for the very clear way to reproduce.

ohler55 avatar Sep 22 '21 13:09 ohler55

https://github.com/ohler55/oj/blob/develop/CHANGELOG.md#3114---2021-04-14

Fixed compatibility issue with Rails and the JSON gem that requires a special case when using JSON.generate versus call to_json on an object.

https://github.com/ohler55/oj/issues/651

Oj.generate now behaves the same as JSON.generate.

If you expect {"post":{"id":1,"title":"Hello","body":"World"}} , Oj.dump({ post: post }) or { post: post }.to_json seems to good to use.

kmasuda-aiming avatar Sep 29 '21 23:09 kmasuda-aiming

@kmasuda-aiming thanks! so in some places I will need to use Oj.dump.

I found an issue with Oj.dump, same reproduction script (I posted on issue body) with oj 3.13.8

and added those lines:

post_obj = { post: post }

puts Oj.generate(post_obj)
puts post_obj.to_json
puts Oj.dump(post_obj)

And Oj.dump fails (Oj.generate and .to_json are working) -

Traceback (most recent call last):
        1: from oj_bug.rb:42:in `<main>'
oj_bug.rb:42:in `dump': Too deeply nested. (NoMemoryError)

If I put mode: :rails in Oj.dump call - it works.

Any idea why?

yosiat avatar Oct 01 '21 08:10 yosiat

Without knowing what the active record layout is I would guess there is an object that implements #as_json by returning itself instead of a Hash or in some cases I have seen it actually calls generate in the #as_json. In rails mode Oj takes some additional steps to catch that. The other modes don't bother as according to the docs #as_json always returns a Hash. Oddly enough active support started the use of #as_json and so far that is the only gem I have seen violate their own documentation.

ohler55 avatar Oct 02 '21 00:10 ohler55

I'm sorry I gave you the wrong information.

In the case of mode: :object, it generates a string like the blew.

{"^o":"Post","new_record":false,"attributes":{"^o":"ActiveModel::AttributeSet","attributes":{"id":{"^o":"ActiveModel::Attribute::FromDatabase","name":"id","value_before_type_cast":1,"type":{"^o":"ActiveRecord::ConnectionAdapters::SQLite3Adapter::SQLite3Integer","precision":null,"scale":null,"limit":null,"range":{"^u":["Range",-9223372036854775808,9223372036854775808,true]}},"original_attribute":null,"value":1},"title":{"^o":"ActiveModel::Attribute::FromDatabase","name":"title","value_before_type_cast":"Hello","type":{"^o":"ActiveModel::Type::String","true":"t","false":"f","precision":null,"scale":null,"limit":null},"original_attribute":null,"value":"Hello"},"body":{"^o":"ActiveModel::Attribute::FromDatabase","name":"body","value_before_type_cast":"World","type":{"^o":"ActiveModel::Type::String","true":"t","false":"f","precision":null,"scale":null,"limit":null},"original_attribute":null,"value":"World"}}},"association_cache":{},"primary_key":"id","readonly":false,"previously_new_record":true,"destroyed":false

mode: :json ( same as mode: :compat ) or mode: :rails generates {"post":{"id":1,"title":"Hello","body":"World"}}.

Oj.dump(post_obj, mode: :rails) to change the mode. If you want to change the settings for the whole system, you can use Oj.default_options = { mode: :rails } .

kmasuda-aiming avatar Oct 03 '21 00:10 kmasuda-aiming

I think this issue can be closed. Any objections?

ohler55 avatar Feb 08 '22 00:02 ohler55