Serialize a summary of a has_many relationship - ruby-on-rails

How can I include a summary of the associated objects rather than the objects itself. For example, if a client has_many projects I could do this:
class ClientSerializer < ActiveModel::Serializer
attributes :id, :name
has_many :projects
end
But this will return all of the associated projects. I would much rather bring back just a count of the projects, the url to download the full list of projects, the last time a project was updated, etc.
What is the best way to include a summary of the associated objects?
Ideally, for example the resulting JSON would look like this:
{
"id": 10,
"name": "My Client",
"projects": {
"count": 5,
"updated_at": "2014-09-09T13:36:20.000-04:00",
"url": "https://my.baseurl.com/clients/10/projects"
}

I'm not sure if this is the best way to do it, but I got this to work:
class ClientSerializer < ActiveModel::Serializer
attributes :id, :name, :archive, :updated_at, :projects
def projects
collection = object.projects.to_a
{ count: collection.length,
updated_at: collection.map(&:updated_at).max,
url: projects_url }
end
end

You could create an instance method:
class ClientSerializer < ActiveModel::Serializer
has_many :projects
def project_count
projects.size
end
end

Related

How to save a nested one-to-many relationship in API-only Rails?

In my Rails (api only) learning project, I have 2 models, Group and Album, that have a one-to-many relationship. When I try to save the group with the nested (already existing) albums, I get the following error, ActiveRecord::RecordNotFound (Couldn't find Album with ID=108 for Group with ID=). I'm using the jsonapi-serializer gem. Below is my current set up. Any help is appreciated.
Models
class Group < ApplicationRecord
has_many :albums
accepts_nested_attributes_for :albums
end
class Album < ApplicationRecord
belongs_to :group
end
GroupsController#create
def create
group = Group.new(group_params)
if group.save
render json: GroupSerializer.new(group).serializable_hash
else
render json: { error: group.errors.messages }, status: 422
end
end
GroupsController#group_params
def group_params
params.require(:group)
.permit(:name, :notes, albums_attributes: [:id, :group_id])
end
Serializers
class GroupSerializer
include JSONAPI::Serializer
attributes :name, :notes
has_many :albums
end
class AlbumSerializer
include JSONAPI::Serializer
attributes :title, :group_id, :release_date, :release_date_accuracy, :notes
belongs_to :group
end
Example JSON payload
{
"group": {
"name": "Pink Floyd",
"notes": "",
"albums_attributes": [
{ "id": "108" }, { "id": "109" }
]
}
}
If the albums already exist, then accepts_nested_attributes is not necessary.
You could save them like this:
Group.new(name: group_params[:name], notes: group_params[:notes], album_ids: group_params[:album_ids])
You will want to extract the album_ids as an array when passing it here.

Creating Nested Relationships for a Rails JSON API with ActiveModelSerializers

I'm trying to build a Rails API with the following JSON structure:
{ team: "team_name",
players: players: [
{
name: "player_one",
contracts: [
{
start_date: '7/1/2017',
seasons: [
{
year: 2017,
salary: 1000000
}
]
}
]
},
{
name: "player_two"
}
]
}
My models are set up as follows:
class Team < ApplicationRecord
has_many :players
end
class Player < ApplicationRecord
belongs_to :team
has_many :contracts
end
class Contract < ApplicationRecord
belongs_to :player
has_many :seasons
end
class Season < ApplicationRecord
belongs_to :contract
end
I'm currently using the following code, but I'd like to clean this up using ActiveModel Serializers (Or other clean solutions)
def show
#team = Team.find(params[:id])
render json: #team.as_json(
except: [:id, :created_at, :updated_at],
include: {
players: {
except: [:created_at, :updated_at, :team_id],
include: {
contracts: {
except: [:id, :created_at, :updated_at, :player_id],
include: {
seasons: {
except: [:id, :contract_id, :created_at, :updated_at]
}
}
}
}
}
}
)
end
Additionally, the array items are showing in descending order by ID, I'm hoping to reverse that. I'm also hoping to order the players by the first contract, first season, salary.
Active Model Serializers is a nice gem to fit into your needs. Assuming you would use 0-10-stable branch (latest: 0.10.7), you can go in this way:
Serializers (Doc)
# app/serializers/team_serializer.rb or app/serializers/api/v1/team_serializer.rb (if your controllers are in app/controllers/api/v1/ subdirectory)
class TeamSerializer < ActiveModel::Serializer
attributes :name # list all desired attributes here
has_many :players
end
# app/serializers/player_serializer.rb
class PlayerSerializer < ActiveModel::Serializer
attributes :name # list all desired attributes here
has_many :contracts
end
# app/serializers/contract_serializer.rb
class ContractSerializer < ActiveModel::Serializer
attributes :start_date # list all desired attributes here
has_many :seasons
end
# app/serializers/season_serializer.rb
class SeasonSerializer < ActiveModel::Serializer
attributes :year, :salary # list all desired attributes here
end
TeamController
def show
#team = Team
.preload(players: [contracts: :seasons])
.find(params[:id])
render json: #team,
include: ['players.contracts.seasons'],
adapter: :attributes
end
Note 1: Using preload (or includes) will help you eager-load multiple associations, hence saving you from N + 1 problem.
Note 2: You may wish to read the details of the include option in render method from Doc
I think your approach is right. Looks clean as it could be.
You can use also named_scopes on your Model.
See docs here.

Use alternate association id for ActiveModel::Serializer association

I have an app where there I have normal ActiveRecord ids as well as a unique field (e.g. ident) that's unique on an external, canonical database. A model looks like:
class Parent
has_many :childs, foreign_key: :parent_ident, primary_key: :ident
end
class Child
belongs_to :parent, foreign_key: :parent_ident, primary_key: :ident
end
For various reasons I'd like the consumer of my Rails API to use the canonical ids (e.g. ident) not the ids defined on the app. So I've defined my serializers (using ActiveModel::Serializer):
class ParentSerializer < ActiveModel::Serializer
attributes :id, :ident, :other, :stuff
has_many :children
def id
object.ident
end
end
class ChildSerializer < ActiveModel::Serializer
attributes :id, ident, :parent_ident, :things
def id
object.ident
end
end
the problem is that the JSON generated correctly is using my overridden IDs for the top-level attributes but the IDs in the child_ids field are the local ids not the canonical idents I want to use.
{
parents: [
{
id: 1234, // overridden correctly in AM::S
ident: 1234,
other: 'other',
stuff: 'stuff',
child_ids: [ 1, 2, 3 ], // unfortunately using local ids
}
],
childs: [
{
id: 2345, // doesn't match child_ids array
ident: 2345,
parent_ident: 1234,
things: 'things'
}
]
}
Question: is there a way to make the parent serializer use the ident field of it's association rather than the default id field?
I have tried putting a def child_ids in the ParentSerializer without success.
I am using Rails 4.2 and the 9.3 version of active_model_serializers gem.
You can specify custom serializers for associations as per the docs
has_many :children, serializer: ChildSerializer

Include properties for ActiveModelSerializer only if called within has many

I have a rails app with the following models.
class Project
has_many :project_clips
has_many :clips, through: :project_clips
end
class Clip
has_many :project_clips
has_many :projects, through: :project_clips.
end
class ProjectSerializer < ActiveModel::Serializer
attributes :id, :name
has_many :clips
end
class ClipSerializer < ActiveModel::Serializer
attributes :id, :name
end
I was wondering if it's possible to display the values of the associated project_clip, if the clip has been called within the context of project.
Let's say the ProjectClip model, has a field called priority. I want the results to show up like this.
{ projects: { "id": 1, "name": "ipsum", "clips": [{ "id": 1, "name": "lorem", "priority": "high"}] } }
I don't want the values of project_clips to be included, just a few properties when returning the data for projects.
If I'm getting your question right, you can do something like:
res = project.clips.collect{ |clip| [clip, clip.project_clips] }
or if you want to return hashes and not objects, you can do:
res = project.clips.collect{ |clip| [clip.attributes, clip.project_clips.collect{|pc| pc.attributes}] }

Rails Active Model Serializer - has_many and accessing the parent record

I'm trying to build a JSON representation of some Rails models using Active Model Serializer, where some models embed others. For example, I have Event and Attendees, Event has_and_belongs_to_many Attendees.
class EventSerializer < ActiveModel::Serializer
attributes :name
has_many :attendees, serializer: AttendeeSerializer
end
class AttendeeSerializer < ActiveModel::Serializer
attributes :name
end
This would result in JSON like { name: 'Event One', attendees: [{ name: 'Alice' }, { name: 'Bob' }] }.
Now, I'd like to add what the attendees have said about the event. Let's say, Comment belongs_to Event, belongs_to Attendee. I'd like to include said comments in the serialized output of event, so it would become { name: 'Event One', attendees: [{ name: 'Alice', comments: [{ text: 'Event One was great!'}] }, { name: 'Bob', comments: [] }] }.
I could have
class AttendeeSerializer < ActiveModel::Serializer
attributes :name
has_many :comments
end
but that would select all the comments by this attendee for all the events - not what I want. I'd like to write this, but how do I find the particular event for which I'm doing serialization? Can I somehow access the 'parent' object, or maybe pass options to a has_many serializer?
class AttendeeSerializer < ActiveModel::Serializer
attributes :name
has_many :comments
def comments
object.comments.where(event_id: the_event_in_this_context.id)
end
end
Is this something that can be done, or should I just build the JSON in another way for this particular use case?
I'd do things manually to get control:
class EventSerializer < ActiveModel::Serializer
attributes :name, :attendees
def attendees
object.attendees.map do |attendee|
AttendeeSerializer.new(attendee, scope: scope, root: false, event: object)
end
end
end
class AttendeeSerializer < ActiveModel::Serializer
attributes :name, :comments
def comments
object.comments.where(event_id: #options[:event].id).map do |comment|
CommentSerializer.new(comment, scope: scope, root: false)
end
end
end

Resources