I made an upgrade of _oj_ from version 2.10.2 to 2.10.4, and I got a serialized object instead of expected JSON.
{
"object": {
"klass": "Client",
"table": {
"name": "clients",
"engine": "Client",
"columns": null,
"aliases": [],
"table_alias": null,
"primary_key": null
},
"values": {
"references": [],
"where": [
{
"left": "#<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x007fdeb9db0b10 @name=\"clients\", @engine=Client(id: uuid, email: string, phone: string, full_name: string, created_at: datetime, updated_at: datetime, service_provider_id: uuid, country_code: string, lang: string), @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>, name=\"service_provider_id\">",
"right": "3ba32f03-0ce0-439d-a26a-8a6617d92116"
}
],
...
}
I use _rails-api 0.3.1_ and _active_model_serializers 0.9.0_
Can you provide a simple test I can run?
@ohler55 I will prepare a test by the end of this week.
I can reproduce this without AMS by doing a simple select in ActiveRecord. Something like User.select("users.first_name || ' ' || users.last_name AS full_name, users.*") behaves the same.
I'm about ready to make a release. There were some changes around ActiveSupport. Maybe that will help.
Anyway, if not, I don't have ActiveRecord setup with a database. A simple test would be one that uses only some number of gems and some ruby code you provide. I'm still not sure what the expected JSON is or how to reproduce what you are doing.
Here's a Rails app with the behaviour: https://github.com/Soliah/oj-test. There are 2 branches, master and no-oj. The only difference betwen the 2 branches is the inclusion of oj in master. The expected output is just the properties of the User model.
Rails 4.1.7, Ruby 2.1.4. http://localhost:3000/users is the test route.
no-oj branch:
[
{
id:5,
first_name:"John",
last_name:"Smith",
email:"[email protected]",
created_at:"2014-11-03T00:39:55.342Z",
updated_at:"2014-11-03T00:39:55.342Z"
}
]
master branch with oj:
{
klass:"User",
table:{
name:"users",
engine:"User",
columns:null,
aliases:[
],
table_alias:null,
primary_key:null
},
values:{
},
offsets:{
},
loaded:false,
arel:{
engine:"User",
ctx:{
source:{
left:{
name:"users",
engine:"User",
columns:null,
aliases:[
],
table_alias:null,
primary_key:null
},
right:[
]
},
top:null,
set_quantifier:null,
projections:[
"#<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x007f847f84b308 @name=" users",
@engine=User(id:integer,
first_name:string,
last_name:string,
email:string,
created_at:datetime,
updated_at:datetime),
@columns=nil,
@aliases= [
],
@table_alias=nil,
@primary_key=nil>,
name="*">"
],
wheres:[
],
groups:[
],
having:null,
windows:[
]
},
bind_values:[
],
ast:{
cores:[
{
source:{
left:{
name:"users",
engine:"User",
columns:null,
aliases:[
],
table_alias:null,
primary_key:null
},
right:[
]
},
top:null,
set_quantifier:null,
projections:[
"#<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x007f847f84b308 @name=" users",
@engine=User(id:integer,
first_name:string,
last_name:string,
email:string,
created_at:datetime,
updated_at:datetime),
@columns=nil,
@aliases= [
],
@table_alias=nil,
@primary_key=nil>,
name="*">"
],
wheres:[
],
groups:[
],
having:null,
windows:[
]
}
],
orders:[
],
limit:null,
lock:null,
offset:null,
with:null
}
}
}
I haven't done anything special in above setup like use AMS. Just a simple render json: @users call.
Since I have not used rails that much it is going to take some time for me to figure out how to get you test working and see what object class is being serialized.
I'm happy to help so please let me know if you want me to try anything.
Please try the latest. I just released 2.11.0.
If that does not work then if you can make a simple ruby script that creates an instance of the object you serialized that would be great. The User object from the looks of it.
I've just tried this in the Rails app and the behavior is the same.
What I'm not clear on is if I'm supposed to call Oj.dump or just some_object.to_json in the context of Rails. Does the oj_mimic_json gem deal with that now that multi_json isn't in the picture?
I've tried reproducing this with ActiveRecord outside of Rails and can't get the same behaviour. This leads me to believe that it's something to do with Rails further up in the rendering section. Maybe something here: https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/renderers.rb though I can't be sure.
I might just work around this and do what was done here http://brainspec.com/blog/2012/09/28/lightning-json-in-rails/
For interests sake here is ActiveRecord without Rails:
source 'https://rubygems.org'
gem "activerecord"
gem "sqlite3"
gem "oj", github: "ohler55/oj"
require 'active_record'
require 'oj'
Oj.mimic_JSON()
# Requiring json here seems to stop conflicts when requiring json in other files.
begin
require 'json'
rescue Exception
# ignore
end
ActiveRecord::Base.logger = Logger.new(STDERR)
ActiveRecord::Base.establish_connection(
:adapter => "sqlite3",
:database => ":memory:"
)
ActiveRecord::Schema.define do
create_table :users do |table|
table.column :first_name, :string
table.column :last_name, :string
table.column :email, :string
end
end
class User < ActiveRecord::Base
end
User.find_or_create_by(first_name: "John", last_name: "Smith", email: "[email protected]")
puts User.first.as_json
The Oj.mimic_JSON should take care of spoofing the json gem. If it is not, let me know and I'll get it back to that state.
There are some things that will help you understand what is going on. Oj has different modes. If you are trying to serialize an object you can use the default :object mode in Oj. As you have seen that creates a JSON representation with all the active record extra stuff in it. Probably not what you want. If you can as_json() and serialize that using the lighting approach you pointed to that should work well. Notice I use "should".
Heres where it might get tricky. the as_json() method is only calling on compat mode. mimic_JSON puts Oj in compat mode though. All good so far. If you have the :use_to_json option set to true the dump should work as expected. If it is false it will most likely not. Can you verify the :use_to_json is true?
Another twist. If to_json is defined and you call that :use_to_json is automatically set to false. This is to avoid the recursive calls active makes when calling to_json. It basically calls the serializer (Oj) which is expected to call to_json. This goes on until the app dies.
The key may be to add another :use_as_json or something. Lets continue exploring.
Ok it seems to me that in Rails, mode: :object is being used no matter what I do when using render json: @users. This can easily be reproduced in rails c with User.first.to_json.
Passing options to to_json doesn't change this behaviour. Calling Oj.default_options = { mode: :compat } explicilty also does nothing to change this behaviour.
However, going Oj.dump(User.first) does behave correctly, so this looks like something with Rails and how JSON is setup?
That is useful information. Passing arguments to to_json() will not work. You can set the default mode like this.
Oj.default_options = { mode: :compat }
From what you describe it sounds like the problem may be in the :use_to_json flag being set to false when to_json() is called. I may have to change that. Can you verify that on Oj.dump(User.first) the as_json() method is called?
Putting a debugger here https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/renderers.rb#L117
the following behaviour is observed:
json.to_json returns https://gist.github.com/Soliah/a5d21eaa77a6762235c1
Oj.dump(json) returns https://gist.github.com/Soliah/5337e30662a9e2828b1e
Oj.dump(json, mode: :object) returns https://gist.github.com/Soliah/8cf38eaec18640582af3
What interesting is json.to_json and Oj.dump(json, mode: :object) returns different values...
The json object is just:
#<ActiveRecord::Relation [#<User id: 5, first_name: "John", last_name: "Smith", email: "[email protected]", created_at: "2014-11-03 00:39:55", updated_at: "2014-11-03 00:39:55">]>
From what you describe it sounds like the problem may be in the :use_to_json flag being set to false when to_json() is called. I may have to change that. Can you verify that on Oj.dump(User.first) the as_json() method is called?
Just confirmed that doing Oj.dump(User.first) does call as_json. as_json doesn't get called during a render json: @users though.
okay, I guess I have to allow as_json. Let me look at some of the issue and make sure as_json does not get into an endless recursive loop with some of the other rails libraries. I the mean time I can create a branch for you with the change if that helps.
One last thing, is this issue has sort of diverged potentially from the original issue with ActiveModelSerializers. I have a feeling it's somewhat related but maybe not completely. I would prefer for this to work with AMS but that might be another issue.
Do you know what the AMS call? I'm sure it is related but not sure the path the AMS is taking.
The current stable version 0.9.x uses as_json as far as I'm aware: https://github.com/rails-api/active_model_serializers/blob/0-9-stable/lib/active_model/default_serializer.rb#L17
master which will be 0.10 is a big refactor from what I understand, so the API will change. @kurko or @guilleiguaran might be able to offer some suggestions.
Try the as_json-ok branch. I think it might be okay. I have a second level check for recursion. The problem is that active support has as_json return the object itself in some cases like for core types.
Ok trying out 00b368cc33bb893bf9ae23f43f70489f7b79f791 (as_json-ok branch)
class User < ActiveRecord::Base
def as_json(options = {})
logger.debug("User as_json called!")
super
end
def to_json(options = {})
logger.debug("User to_json called!")
super
end
end
With render json: User.all, if I have the following in the User model, neither logging statements get hit.
However if I change it to render json: User.first then the logging statement occurs for to_json. Rails obviously has to do something different with User.all as that returns a collection rather than just a User object.
In both cases, as_json doesn't get called at all. Furthemore, none of these cases returns the right JSON output.
Thats not good. There is a way out but it isn't very nice. A dump routine can be registered. It is not efficient though. I suspect the problem is the collection is a Hash or an Array and Active has added a to_json() method that calls the JSON.dump() which is now Oj and Oj then would call the to_json on the object if the :use_to_json was enabled. Works fine up to that point but then the use_to_json is still set to false when trying to convert the User object to json but at that point all attributes will be serialized.
Let me sleep on this and see if I can come up with a work around. I think the key will be getting the as_json methods to be called.
I think @chancancode can help understanding how the to_json/as_json works in the most recent versions of active support :smiley:
@chancancode, can we figure out a way to get this working?
I'll take a look in a few hours :+1:
Thanks, I'll be offline by then but will pick it up in the morning.
Hello!
The situation we got into is quite unfortunate, as we have multiple JSON libraries fighting over each other about very generic method names like to_json :disappointed:
#as_json instead of #to_json#to_json will always go through the Rails encoder (custom pure-Ruby encoder for <= 4.0, json gem for 4.1+), which considers the #as_json hooks among other thingsOj explicitly, developers should call Oj.dump(obj)json gem explicitly, developers should call JSON.generate(obj) (this only work reliably on Rails 4.1+)#to_json definition simply call rails_json_encoder_encode(self.as_json) in all version (slightly simplified)Oj can/should invoke #as_json on objects it doesn't natively know how to encodeBefore we get into the details, here is some backstory and history of how we got ourselves into the current situation.
Once upon a time, Ruby doesn't ship with any JSON library. As Rails needed to generate JSON objects, we wrote our own encoder, and eventualy settled on the to_json-based API as that seemed like the obvious choice at the time. You simply override to_json on your classes, construct a hash to represent your data and recursively call to_json on that Hash, and you have your desired JSON string. (Fun fact: that commit also included the initial JSON decoder that essentially worked by converting JSON into YAML using gsub so it could be parsed with Syck. Fun times!)
At some point, Rubyists started writing other JSON libraries/wrappers that are more efficient than the pure-Ruby encoder that comes with Rails, such as Oj, yajl, etc. There is a problem though – the to_json API does not offer any way for us to cooperate with these libraries. Because to_json is responsible for returning the resulting JSON string, there is no way for these alternative encoders to "hook into" this process.
The key observation here is that most application programmers don't override to_json because they want control of how Ruby objects are _encoded_ into a JSON string; they simply want to describing how they want their data _represented_ (e.g. what fields to include, whether the object should map to an Array or an Object...).
Thus, as_json was born. The idea is that developers can just override as_json to return the Ruby representation of their data (i.e. return a Hash/Array/etc) and let the encoder take care of turning that into the JSON string.
For backwards compatibility reasons, the #to_json method is kept as an "entry point" to our Ruby-based encoder (1.8.7 doesn't have an JSON encoder in the standard library). However, it should now be possible to use an alternative encoder such as Oj, e.g. by calling Oj.dump(some_rails_object_that_oj_doesnt_know_about). In this case, the oj encoder can simply invoke #as_json on the object to get a (hopefully*) simplified representation of the object that it will be able to encode.
(*the as_json hook is provided as a "hint" to the encoder – the developer is free to return any strange objects in the hook and it is up to the encoder to decide what to do with them. In practice though, the meaning of things like symbols, hashes, arrays, booleans and strings are well understood. This is not required to be recurrsive, so if the developer returned, say, an array of non-literals, the encoder should still try to call as_json on its elements. I took some detailed notes about how a reasonable encoder _should_ behave here.)
When Ruby 1.9.1 was released, the json gem officially become part of the Ruby standard library. Since 4.0 dropped support for Ruby 1.8.7, in theory everything should be nice from here on. Unfortunately, it uses the #to_json architecture, which prevented it from being very useful inside Rails because of the flaws described above. Thus things remained largely unchanged.
As the popularity of the JSON gem grew though, another annoying issue arose. As mentioned above, the json gem also defines a to_json method that is not quite the same as the one shipped with Rails. (e.g. one takes an option hash, one expects a State object; one considers as_json, the other doesn't). This created all sorts of problems and confusion.
I picked up the task of cleaning up some of these mess. A few things changed:
State object as the second argument to to_json, where Rails expects a real hash. To avoid these problems, we detect and completely bypass the Rails encoder when the JSON gem is involved. So when you call ActiveSupport::JSON.encode(obj) or obj.to_json, you get the Rails stuff, whereas when you explicitly call JSON.{generate|dump}(obj) you get the "bare" result from JSON gem without any of the Rails stuff (as_json).json gem, there is no point maintaining the pure-Ruby encoder inside Rails anymore. I ripped out all the encoding related stuff inside Rails (i.e. code that converts Ruby object into the actual JSON strings... e.g. nil => "null"), and simply shim'ed the JSON gem. So when you call to_json, Rails would first recursively call as_json on the object and pass that to JSON.generate.json gem. This only affects parsing! In previous versions, Rails has only used multi-json on the parsing side. The encoding side always went through Rails' own encoder (through #to_json) with or without multi-json/oj/json gem activated. Except for the spots that you explicitly used MultiJson.dump(...), you have always been using Rails' internal encoder. Installing Oj gem does not tell Rails to use the Oj encoder regardless of your MultiJson settings. Any effect on _encoding_ speed you noticed is probably psychological :)When you call render json: some_object, Rails will call #to_json on the object, which should go through the Rails encoder out-of-the-box.
These two versions of AM::S has a completely different codebase, but for our purpose they are virtually the same. When you do a render json: object, it tries to wrap object in the appropriate serializer object (which implements as_json), and pass that object (still a Ruby object at this point) to the controller's json renderer, which calls to_json as described above.
If you are using Rails 4.2, you'll need 0.8.2 / 0.9.0 for things to work properly because Rails renamed some of the controller hook methods.
This is another complete rewrite, but currently it defines to_json instead of as_json.
(@guilleiguaran we need to :scissors: these to_json in AM::S in favor of as_json as I pointed out here)
The Rails codebase relies on to_json across the board. By default, this should resolve to the Rails JSON encoder, which uses the as_json hook. It appears that somewhere in the Oj gem it overrides to_json with a definition that ignores the as_json hooks (probably here?).
to_jsonas_jsonrender json: Oj.dump(obj) and render json: Oj.dump(WhateverSerializer.new(obj)) in your controllersThis is pretty much how things always worked with previous versions of Oj. (Again MultiJson never actually helped you for encoding.)
Presumably, you can monkey patch the json renderer to do this automatically for you (in that case the AM::S renderer additions should Just Workâ„¢ also).
This requires patching Oj to offer a mode that would override to_json to encode objects in a Rails-compatible way. This is super invasive and you'll probably end up dealing with a lot of the same bugs that we had dealt with before, so I don't know if I could recommend this.
Also, it's quite likely that we would deviate slightly on the encoding result, but if anything comes up I'm happy to work with you to figure out what the "correct" behavior should be (or whether that's an edge case that should be left undefined).
If supporting only Rails 4.1 and above is an option, there's a slightly less invasive option. (In this case it probably shouldn't try to mimic json gem.) It still has the "deviate slightly on the encoding result" problem, but it'd at least shield you from the json gem compat State object nonsense.
wdyt @jeremy? :trollface:
Fantastic explanation. Thanks.
I was aiming for option 1 with the exception of the to_json override. The override is there now to mitigate the rails monkey patching the core/base types. It may make sense for Oj to change some of the options and their behaviour so that it can be made to work with both Rails and the Active libraries.
I'll make a branch of Oj that follows the semi-formal description for as_json and see how it works. I believe the key difference will be in handling the primitive and base type.
Let me know how it goes! Treat the gist as a "note", more than anything, we can certainly review and fix anything that doesn't make sense.
As for the immediate problem described in this ticket, I am pretty sure it's just that to_json is being overridden with an incompatible definition (as a reporter noted above, Oj.dump and to_json gives different results). So turning that off should fix the problem.
I'm doing some planning and wanted to run a few things past the group. I can't remove the to_json override since many (maybe most) call JSON.dump which creates a recursive loop unless the to_json call turns off Oj calling to_json when it encountered an Object that responds to that method. The alternative would be to never call to_json from within Oj. Any opinions?
The as_json would always be called if the Object responded to that method unless it is a primitive. I will leave in the check for an object returning itself though.
I'm not 100% sure if I understand your problem, can you explain...
to_json override look like in Oj?Here is what the to_json override looks like in recent versions of Rails (4.1+).
to_json at all – we only use as_json for serialization, and to_json is just an entry point to the encoder.JSON.dump (instead of to_json), people probably expect to get the json gem's output (i.e. uses to_json instead of as_json), so we bypass the Rails encoder entirely.I suspect point number 1 is why we don't have the same problem that you are dealing with.
For point number 2, we were able to do that because json gem's to_json has a unique (and incompatible) method signature – it passes a ::JSON::State object instead of a hash to the method. So when we see that the argument is a ::JSON::State object, we know that it's coming from JSON.dump or JSON.generate, in which case we just defer to the original definition of to_json without the Rails override.
I think part of the problem is I'm trying got be compatible with both older and the latest rails.
Maybe the trick is the ::JSON::State object. I'm not exactly sure that works yet though.
It is clear that to_json and as_json need to be considered separately though. Let me try to put something together and see how it works. Not sure if I will get to it tonight to tomorrow.
Simply not turning off as_json with to_json seems to be enough. I have to put together a test with and ActiveSupport collection though. If anyone has simple one I can use that would be great. Otherwise I'll do some web searching and write something tomorrow afternoon.
I'm wondering if yajl json's approach would work here or if it's the same issue:
https://github.com/brianmario/yajl-ruby/tree/master/lib/yajl/json_gem
I'd prefer not to take the yawl/json_gem approach as it forces all object to use to_s as a json representation if ActiveSupport is not defined. Not all Oj users are using ActiveSupport.
Here is a simple test that works but I'm not sure how to make sure Oj is used for the to_json call without using Oj.mimicJSON. It seems to do the right thing in the as_json-ok branch.
require 'sqlite3'
require 'active_record'
require 'oj'
#Oj.mimic_JSON()
Oj.default_options = {mode: :compat, indent: 2}
#ActiveRecord::Base.logger = Logger.new(STDERR)
ActiveRecord::Base.establish_connection(
:adapter => "sqlite3",
:database => ":memory:"
)
ActiveRecord::Schema.define do
create_table :users do |table|
table.column :first_name, :string
table.column :last_name, :string
table.column :email, :string
end
end
class User < ActiveRecord::Base
end
User.find_or_create_by(first_name: "John", last_name: "Smith", email: "[email protected]")
User.find_or_create_by(first_name: "Joan", last_name: "Smith", email: "[email protected]")
puts "as_json - #{User.first.as_json}"
puts "to_json - #{User.first.to_json}"
puts "Oj.dump - #{Oj.dump(User.first)}"
puts "Oj.dump all - #{Oj.dump(User.all)}"
Does the latest release fix this?
I'll give it ago and report back.
Is oj_mimic_json still required?
the mimic_JSON call is needed if you are making JSON calls or calling to_json(). If you are using Oj directly then mimic_JSON is not needed.
Working for myself with active_model_serializers using the current HEAD. Loading about 10k records in development takes about half a second off rendering:
Without oj: Completed 200 OK in 3140ms (Views: 3061.0ms | ActiveRecord: 78.4ms)
With oj: Completed 200 OK in 2483ms (Views: 2406.5ms | ActiveRecord: 76.3ms)
The objects are being serialized correctly with or without AMS.
Excellent!
For smaller data sets the rendering time difference isn't particularly different than the built in JSON library. Is there a way I can confirm that oj is being used?
Try setting the default options indent to 2 and look at the output.
Ok that confirms that oj is definitely working. :heart: :sparkles:
great, are we okay to close this issue?
Yup. Would be great to have a new version up on Rubygems.
2.11.1 is there now. What version have you been using?
Oh sorry, I was just pulling down current HEAD on master (374c1133fb412953cc52ea82884e26bbfbf7c571). The version on Rubygems doesn't give me the right JSON back.
Thats strange. The code is the same except for some travis changes. And the changes to floats tonight.
Sorry, looks like I was referencing the wrong version in my Gemfile.lock. Good to close!
great, thanks
@chancancode @Soliah How does this fix the AMS problem?
Most helpful comment
Hello!
The situation we got into is quite unfortunate, as we have multiple JSON libraries fighting over each other about very generic method names like
to_json:disappointed:Quick Summary
#as_jsoninstead of#to_json#to_jsonwill always go through the Rails encoder (custom pure-Ruby encoder for <= 4.0,jsongem for 4.1+), which considers the#as_jsonhooks among other thingsOjexplicitly, developers should callOj.dump(obj)jsongem explicitly, developers should callJSON.generate(obj)(this only work reliably on Rails 4.1+)#to_jsondefinition simply callrails_json_encoder_encode(self.as_json)in all version (slightly simplified)Ojcan/should invoke#as_jsonon objects it doesn't natively know how to encodeBefore we get into the details, here is some backstory and history of how we got ourselves into the current situation.
Rails JSON encoder
Ancient History (Rails 2.3 era)
Once upon a time, Ruby doesn't ship with any JSON library. As Rails needed to generate JSON objects, we wrote our own encoder, and eventualy settled on the
to_json-based API as that seemed like the obvious choice at the time. You simply overrideto_jsonon your classes, construct a hash to represent your data and recursively callto_jsonon that Hash, and you have your desired JSON string. (Fun fact: that commit also included the initial JSON decoder that essentially worked by converting JSON into YAML usinggsubso it could be parsed with Syck. Fun times!)Rails 3 era
At some point, Rubyists started writing other JSON libraries/wrappers that are more efficient than the pure-Ruby encoder that comes with Rails, such as
Oj,yajl, etc. There is a problem though – theto_jsonAPI does not offer any way for us to cooperate with these libraries. Becauseto_jsonis responsible for returning the resulting JSON string, there is no way for these alternative encoders to "hook into" this process.The key observation here is that most application programmers don't override
to_jsonbecause they want control of how Ruby objects are _encoded_ into a JSON string; they simply want to describing how they want their data _represented_ (e.g. what fields to include, whether the object should map to an Array or an Object...).Thus,
as_jsonwas born. The idea is that developers can just overrideas_jsonto return the Ruby representation of their data (i.e. return a Hash/Array/etc) and let the encoder take care of turning that into the JSON string.For backwards compatibility reasons, the
#to_jsonmethod is kept as an "entry point" to our Ruby-based encoder (1.8.7 doesn't have an JSON encoder in the standard library). However, it should now be possible to use an alternative encoder such as Oj, e.g. by callingOj.dump(some_rails_object_that_oj_doesnt_know_about). In this case, the oj encoder can simply invoke#as_jsonon the object to get a (hopefully*) simplified representation of the object that it will be able to encode.(*the
as_jsonhook is provided as a "hint" to the encoder – the developer is free to return any strange objects in the hook and it is up to the encoder to decide what to do with them. In practice though, the meaning of things like symbols, hashes, arrays, booleans and strings are well understood. This is not required to be recurrsive, so if the developer returned, say, an array of non-literals, the encoder should still try to callas_jsonon its elements. I took some detailed notes about how a reasonable encoder _should_ behave here.)Rails 4 era
When Ruby 1.9.1 was released, the
jsongem officially become part of the Ruby standard library. Since 4.0 dropped support for Ruby 1.8.7, in theory everything should be nice from here on. Unfortunately, it uses the#to_jsonarchitecture, which prevented it from being very useful inside Rails because of the flaws described above. Thus things remained largely unchanged.As the popularity of the JSON gem grew though, another annoying issue arose. As mentioned above, the
jsongem also defines ato_jsonmethod that is not quite the same as the one shipped with Rails. (e.g. one takes an option hash, one expects aStateobject; one considersas_json, the other doesn't). This created all sorts of problems and confusion.Rails 4.1+
I picked up the task of cleaning up some of these mess. A few things changed:
Stateobject as the second argument toto_json, where Rails expects a real hash. To avoid these problems, we detect and completely bypass the Rails encoder when the JSON gem is involved. So when you callActiveSupport::JSON.encode(obj)orobj.to_json, you get the Rails stuff, whereas when you explicitly callJSON.{generate|dump}(obj)you get the "bare" result from JSON gem without any of the Rails stuff (as_json).jsongem, there is no point maintaining the pure-Ruby encoder inside Rails anymore. I ripped out all the encoding related stuff inside Rails (i.e. code that converts Ruby object into the actual JSON strings... e.g.nil=>"null"), and simply shim'ed the JSON gem. So when you callto_json, Rails would first recursively callas_jsonon the object and pass that toJSON.generate.jsongem. This only affects parsing! In previous versions, Rails has only used multi-json on the parsing side. The encoding side always went through Rails' own encoder (through#to_json) with or without multi-json/oj/json gem activated. Except for the spots that you explicitly usedMultiJson.dump(...), you have always been using Rails' internal encoder. Installing Oj gem does not tell Rails to use the Oj encoder regardless of your MultiJson settings. Any effect on _encoding_ speed you noticed is probably psychological :)Rails controller
When you call
render json: some_object, Rails will call#to_jsonon the object, which should go through the Rails encoder out-of-the-box.Active Model Serializer
v0.8 & 0.9
These two versions of AM::S has a completely different codebase, but for our purpose they are virtually the same. When you do a
render json: object, it tries to wrapobjectin the appropriate serializer object (which implementsas_json), and pass that object (still a Ruby object at this point) to the controller's json renderer, which callsto_jsonas described above.If you are using Rails 4.2, you'll need 0.8.2 / 0.9.0 for things to work properly because Rails renamed some of the controller hook methods.
v0.10
This is another complete rewrite, but currently it defines
to_jsoninstead ofas_json.(@guilleiguaran we need to :scissors: these
to_jsonin AM::S in favor ofas_jsonas I pointed out here)Where things went wrong with Oj + Rails
The Rails codebase relies on
to_jsonacross the board. By default, this should resolve to the Rails JSON encoder, which uses theas_jsonhook. It appears that somewhere in the Oj gem it overridesto_jsonwith a definition that ignores theas_jsonhooks (probably here?).Where does that leave us...
Option 1: Explicitly opt-in
to_jsonas_jsonrender json: Oj.dump(obj)andrender json: Oj.dump(WhateverSerializer.new(obj))in your controllersThis is pretty much how things always worked with previous versions of Oj. (Again MultiJson never actually helped you for encoding.)
Presumably, you can monkey patch the json renderer to do this automatically for you (in that case the AM::S renderer additions should Just Workâ„¢ also).
Option 2: Automatically use Oj for everything...
This requires patching Oj to offer a mode that would override
to_jsonto encode objects in a Rails-compatible way. This is super invasive and you'll probably end up dealing with a lot of the same bugs that we had dealt with before, so I don't know if I could recommend this.Also, it's quite likely that we would deviate slightly on the encoding result, but if anything comes up I'm happy to work with you to figure out what the "correct" behavior should be (or whether that's an edge case that should be left undefined).
Option 3: Automatically use Oj for everything (Rails 4.1+ only)...
If supporting only Rails 4.1 and above is an option, there's a slightly less invasive option. (In this case it probably shouldn't try to mimic json gem.) It still has the "deviate slightly on the encoding result" problem, but it'd at least shield you from the json gem compat
Stateobject nonsense.