active_model_serializers icon indicating copy to clipboard operation
active_model_serializers copied to clipboard

Serializer.attributes fails with unclear message when an attribute is `:method`

Open dongtong opened this issue 8 years ago • 13 comments

Expected behavior vs actual behavior

If attributes list has keyword, the serializer will be failed, the error is

Rendered ActiveModel::Serializer::CollectionSerializer with ActiveModelSerializers::Adapter::Attributes ArgumentError: wrong number of arguments (given 0, expected 1)

class ApiSerializer < ApplicationSerializer
  attributes :id,
             :group_name,
             :name,
             :endpoint,
             :method,
             :version,
             :created_at,
             :updated_at,
             :description
  #...
end

Solutions

class ApiSerializer < ApplicationSerializer
  attributes :id,
             :group_name,
             :name,
             :endpoint,
             :version,
             :created_at,
             :updated_at,
             :description
  attribute :method

  def method
     object.method
  end
end

I think active_model_serializers should tell the developer that attributes should not include keywords, otherwise, the developer will not know the error's detail information. If I have time, I will create PR, but I want the library creator could improve this issue.

Thanks a lot

dongtong avatar Jan 30 '17 18:01 dongtong

@dongtong Thanks for the report. It would be great if you could submit a PR to improve the error message. We actually added a 'block format' for attributes for just this scenario:

class ApiSerializer < ApplicationSerializer
  attribute :method do
    object.method
  end

I'm not sure of a good way to warn people not to use words that might already be defined methods like select and method. We could check if the method is defined in the serializer, but it's probably easier to just fail early with a good message.

bf4 avatar Jan 30 '17 18:01 bf4

Another hour wasted with this gem before I find this issue. I really regret ever using it

9mm avatar Feb 19 '17 17:02 9mm

@9mm not gem's fault. We can make it harder to have this problem in gem, but is ultimately devs needing to avoid names that are already defined: method, select, display etc. If you didn't experience this problem in ams you'd find it elsewhere. Same reason we always use self with setters.

bf4 avatar Feb 19 '17 19:02 bf4

@bf4 I'm not sure if using keywords or "names that are already defined" is the issue here; it's more about what these definitions are. Errors when calling method or select from the serializer are raised because methods require arguments and/or a block (ArgumentError). In case of methods with no (or default) arguments serialization works fine, even when using reserved words as attribute names.

The problem here is to distinguish which methods can be called and which cannot. I created a more-or-less naive fix (https://github.com/xlts/active_model_serializers/blob/invalid-attribute-name/lib/active_model/serializer.rb#L379) which works fine... until the model's read_attribute_for_serialization is called in when using attributes such as select. As it turns out, for this method ActiveModel::Serialization simply defines an alias to send (see https://github.com/rails/rails/blob/92703a9ea5d8b96f30e0b706b801c9185ef14f0e/activemodel/lib/active_model/serialization.rb#L168), pretty much repeating this gem's logic. So I guess the ultimate fix will not be that simple considering the dependency on ActiveModel. In any case, I'd happily provide a solution if we think of one.

xlts avatar May 08 '17 18:05 xlts

@xlts In 0.10.6 I undef'd display and select. I considered undeffing method more dangerous. A lot of code could depend on that.

In my dev work I'm inheriting from BasicObject, but even BasicObject has method. The fix that I made in dev is that attribute declaration creates an instance method that delegates to the instance object, so it would create a method def method; object.method; end which would work as long as no one needs to use Kernel.method. Calling thing method, in general, is asking for trouble, from my own experience.

bf4 avatar May 08 '17 18:05 bf4

@bf4 allright, that makes sense and seems much less error-prone. So I guess this issue is more or less dealt with.

xlts avatar May 08 '17 20:05 xlts

@xlts Thanks for your understanding. I wish code always just did what we intended :) Let us know what you end up doing.

bf4 avatar May 08 '17 21:05 bf4

@bf4 I suppose I'll wait for a new release as you tackled the issue already. Unless there is something else I can help with?

xlts avatar May 10 '17 13:05 xlts

I'll wait for a new release

Or just define your attribute with a block attribute(:method) { object.method }.

Don't wait the the release :)

bf4 avatar May 11 '17 02:05 bf4

@bf4 Obviously that's what I'm doing :). I meant I'm waiting for the general solution, that's all.

xlts avatar May 11 '17 13:05 xlts

IMO the reason for this behavior is poor design. A possible fix would be for AMS to automatically define a dummy method that just forwards to the object whenever an attribute is declared without a block. It would be dangerous as well, but much more consistent with the idea that "attributes are methods on the serializer", compared to the current concept of "attributes are something, unless a method is defined, in which case they are methods".

beauby avatar May 22 '17 17:05 beauby

A possible fix would be for AMS to automatically define a dummy method that just forwards to the object whenever an attribute is declared without a block

So, now it defines a method unless one exists. You're suggesting that it always define an instance method. It could still be overridden, so I think that would work fine. It would mostly be a problem with inheritance if that same attribute were declared more than once.

bf4 avatar May 22 '17 18:05 bf4

I has a similar problem: serializer a object attribute.

If use:

attributes :object

then the object attribute of generated json will contain all attribute of instance be serializered.

So has to define like this :

attribute(:object) {object.object}

cocoakekeyu avatar Dec 13 '17 08:12 cocoakekeyu