-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
- Start Date: (2016-09-14)
- RFC PR: N/A
- ActiveModelSerializers Issue: Inheriting from ActiveModelSerializers::Model to support serialization of a PORO is a bad practice #1877
Summary
The documentation on serializing POROs leads one to believe the PORO should subclass ActiveModelSerializers::Model. There are three issues with this:
- Subclassing is a bad practice when a mixin will do. What if I already have a superclass?
ActiveModelSerializers::Modelprovides a bunch of extra functionality that is not needed by this library. So POROs are polluted for no reason.- This gives the false impression that
ActiveModelSerializers::Modelor the contract that it provides is somehow relevant to the rest of this library.
Motivation
-
Because as far as I can tell AMS does not need objects to implement this interface. Maybe at one point it did, but this test shows POROs can be serialized without any special mixin, interface, etc.
The only code change to make that work is:
def read_attribute_for_serialization(attr) if respond_to?(attr) send(attr) else - object.read_attribute_for_serialization(attr) + if object.respond_to?(:read_attribute_for_serialization) + object.read_attribute_for_serialization(attr) + else + object.send(attr) + end end endSo to me it's like we might as well lint that these objects respond to
foo; it's arbitrary. -
This provides a better experience to end users. No special steps required to serialize POROs.
-
Currently the documentation is misleading / point of
ActiveModelSerializers::Modelis misleading. It says in order to serialize POROs I should subclass this, but it actually adds a lot of stuff to my object I may not want (polluting my PORO). For instance these objects now respond toupdated_at...I would never expect that as an end user. In addition, most of my objects are Virtus objects, which has its own@attributesimplementation that just happens to work right now but is ripe for future conflicts. -
The logic for constructors/attributes/errors etc might be nice to have, but it's not relevant to a serialization library. I would rather this move to something like an extension.
-
The fact that we're having this discussion is one of the problems. Yeah, we lint these objects quack like a certain kind of duck, but nowhere do we assert why they have to quack that way. If I implemented a feature that needed objects to respond to
as_json, but a few months later that feature gets removed, nobody knows if we can removeas_jsonfrom the lint or not. -
Begins to remove Rails dependencies, so this library could be used in Sinatra et al without requiring ActiveModel and friends.
-
I prefer a more composable approach so I pollute my POROs the minimal amount possible. I'll start with a PORO that can be serialized by default, but I'll add
ActiveModelSerializers::CacheSerializableif my serializer needs the caching feature. Or (this would be my preference) we can implement caching so that it doesn't have this requirement at all. For instance it seems easy to putcache_keyas a method on a serializer, instead of on the object we are serializing.
Detailed design
Once again, make this code change:
def read_attribute_for_serialization(attr)
if respond_to?(attr)
send(attr)
else
- object.read_attribute_for_serialization(attr)
+ if object.respond_to?(:read_attribute_for_serialization)
+ object.read_attribute_for_serialization(attr)
+ else
+ object.send(attr)
+ end
end
endThen, create ActiveModelSerializers::CacheSerializable and ActiveModelSerializers::ErrorSerializable. These will implement only the bits of ActiveModelSerializers::Model that are needed for those purposes.
If possible, both those mixins should be immediately deprecated as well. For instance it seems like an easy change to use the serializer's updated_at instead of requiring the object respond to updated_at. This would be better code in any case. Suggest these conversations happen in separate, targeted PRs.
Finally, remove the lint tests since these are both unnecessary and misleading.
Drawbacks
We'd probably add something to our test POROs so they could keep the otherwise-irrelevant constructor functionality ActiveModelSerializers::Model provides. So you could make a case our tests wouldn't be testing on true POROs.
Suggest handling this by adding a separate test(s) specific to vanilla POROs.
Alternatives
We could avoid the code change mentioned above, and instead implement a mixin that does the same:
module ActiveModelSerializers
module Serializable
def self.included(klass)
klass.class_eval do
alias :read_attribute_for_serialization, :send
end
end
end
endI would support this alternative, though I think it's kind of silly and puts extra work on our users without justification.
Unresolved questions
In the 0.8x series, my_model.as_json would use AMS to serialize. This functionality is not currently in 0.10.x, though there are many comments about doing so.
I suggest this is the true purpose of a future ActiveModelSerializers::Serializable mixin, that should be independent of this issue (and completely optional to users). If that's not the case, I imagine we would double down on ActiveModelSerializers::Model and monkey-patch ActiveRecord...so the unresolved question would be "is that really a good idea"...
Additional Information
Prior discussion here: #1911