active_model_serializers
active_model_serializers copied to clipboard
Define a context for a nested association
Expected behavior vs actual behavior
From the serializer of an association, I would like to access the object of the parent serializer.
Steps to reproduce
Say we have three models, Ballot
, Candidate
, and Vote
. Candidate
s and Ballot
s have a many to many relationship. Vote
s belong to a Ballot
and a Candidate
. I'm writing a Ballot
serializer that has_many :candidates
. I'm then making use of the subclassed serializer to have a custom CandidateSerializer
. Inside the CandidateSerializer
I want an attribute that represents the number of votes for that Candidate
on that Ballot
. But to write that method, I need to know which ballot this is being serialized for. Is this possible somehow?
I have managed to hack it by removing the association on the BallotSerializer
and just defining a candidates
attribute that returns an array of hashes, and build the candidate
that way. But having the child serializer seems a lot cleaner. Is there anyway for me to pass that context of which ballot I'm serializing this candidate for?
Environment
ActiveModelSerializers Version (commit ref if not on tag): a6c6979e081fe5c40c471a5fe7cd9d305a471d15
Output of ruby -e "puts RUBY_DESCRIPTION"
: ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin15]
OS Type & Version: OS X 10.11.3
Integrated application and version (e.g., Rails, Grape, etc): Rails 5.0.0.beta2
Additonal helpful information
I am currently using the default attributes
adapter.
Let me know if there's anything I can post to make the above situation more clear. Thanks for your help!
@willcosgrove Thanks for the issue!
I would like to access the object of the parent serializer. I want an attribute that represents the number of votes for that Candidate on that Ballot. But to write that method, I need to know which ballot this is being serialized for
could you maybe give some more complete code examples? I'm not sure I understand the data model. If you can sketch out the model classes and serializer classes that would be helpful. Are you just asking if you can has_many :ballots { object.ballot.total_votes(object) }
?
Yeah definitely. So say I have these serializers:
class BallotSerializer < ActiveModel::Serializer
attributes :id, :name, :owner_id, :created_at, :updated_at
has_many :candidates
class CandidateSerializer < ActiveModel::Serializer
attributes :id, :name, :vote_count
def vote_count
# current_ballot is what I want to be able to access, but I'm at a loss for how
object.votes.where(ballot_id: current_ballot.id).count
end
end
end
Does that help explain the problem? From within the vote_count
method of the CandidateSerializer
I currently have no way of knowing for which Ballot the candidate is being serialized.
@willcosgrove there is something that is not clear here. Does a Candidate
model have access to its ballot
?
- If it does then I assume you can use:
def vote_count
object.votes.where(ballot_id: object.ballot.id).count
end
- If it does not, then there is an easy workaround in your case. You can use the
instance_options
which is passed to the association serializer, see #1545 for documentation and check out this gist as example.
@bf4 Another solution here would be to have access to the parent serializer within an association. The only issue I see is that it would only work (nicely) for serializers that would not be serialized outside of the scope of their parent serializer. If the association use a standalone serializer (not embedded within a parent serializer), an if
statement would be required to check whether or not it is being serialized within the scope of a parent serializer. I guess that having a parent
or parent_serializer
method that defaults to nil
would be good practical solution. What you would have in such case is:
class CandidateSerializer < ActiveModel::Serializer
attributes :id, :name
attribute :vote_count, if: :has_parent? do
object.votes.where(ballot_id: parent.object.id).count
end
def has_parent?
!parent.nil?
end
end
class BallotSerializer < ActiveModel::Serializer
attributes :id, :name, :owner_id, :created_at, :updated_at
has_many :candidates
end
@groyoh Wow, thanks for all your help! I'll try to respond to each thing in order.
Does a
Candidate
model have access to itsballot
?
It does not, because a candidate can have multiple ballots. So you can't call candidate.ballot
but you can call candidate.ballots
. Unfortunately that doesn't get you much closer to figuring out which ballot is being serialized.
You can use the
instance_options
which is passed to the association serializer
This sounds like my best bet currently. I don't really like that it has to be specified in the controller though. I would prefer to specify it inside the serializer, when I define the relationship:
has_many :candidates, ballot: object
The one glaring problem there is that I don't have access to the object
in the class scope. So I would have to give it a symbol or a proc, and have the gem evaluate it for me in the scope of the instance.
Having to define it at the controller level seems confusing
render json: @ballot, ballot: @ballot
My current work-around is to change the candidates
from an association, to an attribute that just returns an array of hashes, rather than go through another CandidateSerializer
. That way I'm still in the scope of the BallotSerializer
and I can still access the object
.
Another solution here would be to have access to the parent serializer within an association
This would be my preferred way of dealing with it.
Thanks again everyone for all your help! I've gotten everything I need from this issue, but I'll leave it open in case there is interest in further discussing the parent
idea. If not, feel free to close this issue.
@willcosgrove thanks for the explanation. It's nice to have such a nice issue description and details.
has_many :candidates, ballot: object
From your example, I also figured out that it would also be possible to add a new method to the association DSL in order to pass some arbitrary options like so:
has_many :candidates do
instance_options ballot: object
end
@bf4 @willcosgrove how do you feel about such solution? I think that this is something that might actually add some values to the association without messing up with the serializer class itself.
@groyoh that looks great to me!
Another solution here would be to have access to the parent serializer within an association. The only issue I see is
I think the problem is having a recursive dependency. How could you count votes outside od the serializer, in a one liner?
ballot.candidates.map {|c| c.votes(ballot) }
Right?
On Sat, Mar 5, 2016 at 12:22 PM Will Cosgrove [email protected] wrote:
@groyoh https://github.com/groyoh that looks great to me!
— Reply to this email directly or view it on GitHub https://github.com/rails-api/active_model_serializers/issues/1554#issuecomment-192702176 .
@bf4 I'm not sure I'm following what you're saying. A recursive dependency is always a problem, I'm not sure how something like @groyoh proposed makes it any more of a problem.
@willcosgrove I believe you can now do what you want now that https://github.com/rails-api/active_model_serializers/pull/1633 has been merged
Oh sweet, I will check it out. I'll go ahead and close this issue then. Thanks for your help!
Hey, sorry to reopen this issue. I've gotten a chance to play around with the stuff added in #1633 and it looks great, but I can't find a way to set the instance options of the serializer. I can set the reflection options, but those don't get copied over to the instance options of the serializer.
def serializer_options(subject, parent_serializer_options, reflection_options)
serializer = reflection_options.fetch(:serializer, nil)
serializer_options = parent_serializer_options.except(:serializer)
serializer_options[:serializer] = serializer if serializer
serializer_options[:serializer_context_class] = subject.class
serializer_options
end
Source: reflection.rb:137-144
So that means when I'm defining a reflection:
has_many :candidates do |serializer|
# Would love to do this but cannot, instance_options is not an available method
# instance_options ballot: serializer.object
# Can do this, but it doesn't do anything helpful to me, currently
self.options = {ballot: serializer.object}
end
Hopefully I haven't missed something simple that would let me do this 😁 . Let me know if I can provide further clarification on what I'm looking for.
Thanks!
@bf4 this also isn't working for me. I can do the following:
has_many :assessments
def assessments
object.assessments.where(vendor_id: instance_options[:vendor_id])
end
but when I try to simplify and do:
has_many :assessments do |object|
object.assessments.where(vendor_id: instance_options[:vendor_id])
end
I get undefined method 'assessments' for #<VendorProjectSerializer>
There no yielded object so the blockparam is nil. Aredocs wrong somewhere?
B mobile phone
On Jan 27, 2017, at 12:06 PM, Stefan Wrobel [email protected] wrote:
@bf4 this also isn't working for me. I can do the following:
has_many :assessments def assessments object.assessments.where(vendor_id: instance_options[:vendor_id]) end but when I try to simplify and do:
has_many :assessments do |object| object.assessments.where(vendor_id: instance_options[:vendor_id]) end I get undefined method 'assessments' for #<VendorProjectSerializer>
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.
I don't understand your question
You wrote
has_many :assessments do |object|
But should have
has_many :assessments do
And I asked if you wrote the wrong way, w block param, due to docs
B mobile phone
On Jan 27, 2017, at 6:38 PM, Stefan Wrobel [email protected] wrote:
I don't understand your question
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.
I suppose upon re-reading the docs, they are clear that the block param is the serializer itself, not the object.
When I try
has_many :assessments do
object.assessments.where(vendor_id: instance_options[:vendor_id])
end
I get undefined local variable or method 'instance_options' for #<ActiveModel::Serializer::HasManyReflection>
@swrobel so, we can add that as a private method in the Reflection class, or right now you can add the 'serializer' block param call serializer.send(:instance_options).fetch(:vendor_id)
. I agree this is a usability issue.
Came across this quirk myself today too. Would be nice to look at a permanent solution to get instance_options
available locally in the has_many block rather than accessing through the protected method in the serializer.
@willcosgrove Here's what I did to solve a similar problem (adapted to your models):
has_many :candidates do |serializer|
# Set instance option for parent serializer, the option will be passed to the child serializer
serializer.send(:instance_options)[:ballot] = serializer.object
# Find the records for the association
serializer.object.ballots
end
It would be usable addition though if instance options could be set for only the nested serializers.
Not sure if this is any better but I ended up doing the following
def ballot(value)
instance_options[:ballot] = value
end
has_many :candidates do |serializer|
# Set instance option for parent serializer, the option will be passed to the child serializer
ballot(serializer.object)
# Find the records for the association
serializer.object.ballots
end
Any update on this?
BWT If you don't want to mutate original collection, just return :nil
has_many :candidates do |serializer|
# Set instance option for parent serializer, the option will be passed to the child serializer
serializer.send(:instance_options)[:ballot] = serializer.object
:nil
end
.send(:instance_options)[:ballot] = serializer.object
This is very neat. why :nil
though?