Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rendering array objects that doesn't have serializers #962

Merged
merged 8 commits into from Jun 26, 2015

Conversation

joaomdmoura
Copy link
Member

As reported on #772 and #945.
There was a crash when rendering arrays that don't consist of "serializables" classes.
This PR is a first implementation to quickly fix it.

P.S. It won't fix #877 yet.
P.S. #954 will need to be updated based on this once merged, around here

@bf4
Copy link
Member

bf4 commented Jun 22, 2015

ref: #955 (comment)

and a fix I wrote that works at a different level, in the serializer itself.

56a7bd1

# https://github.com/rails-api/active_model_serializers/blob/56a7bd1b6dd9f083b0f3a9d6d24e9dc85efbffac/lib/active_model/serializer.rb
    def each_association(&block)
      self.class._associations.dup.each do |name, association_options|
        next unless object
        association_value = send(name)

        serializer_class = ActiveModel::Serializer.serializer_for(association_value, association_options)

         if serializer_class
-          serializer = serializer_class.new(
-            association_value,
-            options.except(:serializer).merge(serializer_from_options(association_options))
-          )
+          begin
+            serializer = serializer_class.new(
+              association_value,
+              options.except(:serializer).merge(serializer_from_options(association_options))
+            )
+          rescue NoMethodError
+            # 1. Failure to serialize an element in a collection, e.g. [ {hi: "Steve" } ] will fail
+            #    with NoMethodError when the ArraySerializer finds no serializer for the hash { hi: "Steve" },
+            #    and tries to call new on that nil.
+            # 2. Convert association_value to hash using implicit as_json
+            # 3. Set as virtual value (serializer is nil)
+            # 4. Consider warning when this happens
+            virtual_value = association_value
+            virtual_value = virtual_value.as_json if virtual_value.respond_to?(:as_json)
+            association_options[:association_options][:virtual_value] = virtual_value
+          end

@joaomdmoura
Copy link
Member Author

@bf4, indeed a better solution, tried to backport it, but it didn't fixed the problem.
test failed:

def render_json_array_object_without_serializer
  render json: [{error: 'Result is Invalid'}]
end

...

def test_render_json_array_object_without_serializer
  get :render_json_array_object_without_serializer

  assert_equal 'application/json', @response.content_type
  assert_equal ([{error: 'Result is Invalid'}]).to_json, @response.body
end

@bf4
Copy link
Member

bf4 commented Jun 22, 2015

@joaomdmoura Yeah, looking at that now, was going to make a PR to your branch

@joaomdmoura
Copy link
Member Author

@bf4 tks! I was working on it too, but no worries 😄 make yourself comfortable just let me know if you need help or anything.

@bf4
Copy link
Member

bf4 commented Jun 22, 2015

they're closely related, but have somewhat different paths, both triggered by serializable_hash, but no common place that's called, ugh. I think I'm trying to do too much at once by combining those tests... gonna make the PR just for discussion joaomdmoura#1

@bf4
Copy link
Member

bf4 commented Jun 22, 2015

So, I think I got it figured out in that PR. Basically, the special case can be boiled down to ActiveModel::Serializer.serializer_for(object) returning an ActiveModel::Serializer.config.array_serializer that has elements that have no serializer i.e.

object.any? {|elem| ActiveModel::Serializer.serializer_for(object).nil? }

@bf4
Copy link
Member

bf4 commented Jun 22, 2015

So, I'm thinking maybe the solution should be in serializer_for rather than in ArraySerializer, but I'm not sure how to efficiently do that without essentially iterating over all the elements twice.

@joaomdmoura joaomdmoura force-pushed the render-array-objects branch 3 times, most recently from 3f49f51 to 15318b7 Compare June 22, 2015 21:00
include Enumerable
delegate :each, to: :@objects

attr_reader :meta, :meta_key
attr_reader :meta, :meta_key, :objects
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to keep objects reader around? It's no longer used. I could see an argument that it's a private interface.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, indeed, there is no reason to keep it

fail NoSerializerError, "No serializer found for object: #{object.inspect}"
else
serializer_class.new(object, options.except(:serializer))
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about having serializer_for return nil for if any elements don't have an associated serializer? That would maintain current PR behavior, I think, might make more sense SRP-wise, but might be a performance hit.

Should we add a test for a collection or association that contains an element with a known serializer and another with a nil serializer class? I'm really not sure how to handle that in AMS without more complexity. I'd rather push that back to the end user, but that could be laziness.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would need to update this PR if we change this behaviour at serializer_for.

I think it's an edge-case. I haven't heard about ppl using AMS to renders arrays that mix serializable and non-serializable objects yet, so let's not worry about it for now 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

winning

super(resource, options)
begin
serialized = serializer.new(resource, @_serializer_opts)
rescue ActiveModel::Serializer::ArraySerializer::NoSerializerError
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add some docs on building your own array serializer as described in https://github.com/rails-api/active_model_serializers#2-for-an-array-resource and the use of this exception class at some point. Want me to make an issue?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on a quest to keep the readme as clean as possible, but I agree that this can be helpful for some ppl, like others details that are poping up.
I'm thinking about start the AMS wiki (using gh wiki feature). For now, make yourself comfortable to open the issue to make sure we won't forget it while I think it trough.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like wikis, but then it's another thing to garden. For AMS I think it could be a plus.

@bf4
Copy link
Member

bf4 commented Jun 23, 2015

Failure are Rails 4.0 only and have to do with deep munging of json keys. I think you can configure that with config.action_dispatch.perform_deep_munge = false ? rails/rails#13420 ? or maybe not? http://guides.rubyonrails.org/4_1_release_notes.html#action-pack-notable-changes

@bf4
Copy link
Member

bf4 commented Jun 26, 2015

Submitted PR to this branch in joaomdmoura#2

joaomdmoura and others added 3 commits June 26, 2015 02:16
Comparing as a JSON string vs. as the Hash that is convert to JSON
works around the different Hash representations.

This likely has to do with the introduction of
config.action_dispatch.perform_deep_munge in Rails 4.1
See Rails issue 13420

  1) Failure:
  ActiveModel::Serializer::Adapter::Json::HasManyTestTest#test_has_many_with_no_serializer
  [active_model_serializers/test/adapter/json/has_many_test.rb:36]:
  --- expected
  +++ actual
  @@ -1 +1 @@
  -{:id=>42, :tags=>[{"attributes"=>{"id"=>1, "name"=>"#hash_tag"}}]}
  +{:id=>42, :tags=>[{"attributes"=>{:id=>1, :name=>"#hash_tag"}}]}

  2) Failure:
  ActiveModel::Serializer::AssociationsTest#test_has_many_with_no_serializer
  [active_model_serializers/test/serializers/associations_test.rb:74]:
  --- expected
  +++ actual
  @@ -1 +1 @@
  -[{"attributes"=>{"name"=>"#hashtagged"}}]
  +[{"attributes"=>{:name=>"#hashtagged"}}]
@joaomdmoura
Copy link
Member Author

Great team work here folks! Tks so much @JustinAiken and @bf4
I'm merging it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[0.10.0rc1] has_many doesn't work without serializer defined on association
3 participants