oj: Rendering JSON with ActiveModel::Serializers returns object
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
About this issue
- Original URL
- State: closed
- Created 10 years ago
- Reactions: 1
- Comments: 55 (26 by maintainers)
Commits related to this issue
- upgrading to Oj gem version 3.0 related: https://github.com/ohler55/oj/issues/199 — committed to transitland/transitland-datastore by drewda 7 years ago
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đ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 âď¸ 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.