How to format Json to not nest associations with ActiveModel::Serializer - ruby-on-rails

I'm using ActiveModel::Serializer in a rails application to format my model data as a json response, but I would like to change the formatting so that the associations of my main model are not nested. I tried setting root: false and that doesn't work
Expected behavior vs actual behavior
I have a model Account with an association belongs_to :account_status
and I was able to add this association in the AccountSerializer to get that associated data just fine. But do to my api contract requirements, I need the json to be formatted without the association nesting.
So I'm getting this:
{
"account_id": 1
<other account info>
...
"account_status": {
"status_code": 1
"desc": "status description"
....
}
}
But I want this:
{
"account_id": 1
<other account info>
...
"account_status_status_code": 1
"account_status_desc": "status description"
....
}
Model + Serializer code
How can I achieve the expected behavior without writing each account_status field as an individual attribute in the AccountSerializer ??
Controller
class AccountsController < ActionController::API
def show
account = Account.find(params[:account_id])
render json: account
end
end
Model
class Account < ActiveRecord::Base
self.primary_key = :account_id
belongs_to :account_status, foreign_key: :account_status_code, inverse_of: :accounts
validates :account_status_code, presence: true
end
Serializer
class AccountSerializer < ActiveModel::Serializer
attributes(*Account.attribute_names.map(&:to_sym))
belongs_to :account_status,
foreign_key: :account_status_code,
inverse_of: :accounts
end
Environment
OS Type & Version: macOS Catalina v 10.15.7
Rails 6.1.4:
ActiveModelSerializers Version 0.10.0:
Output of ruby -e "puts RUBY_DESCRIPTION":
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-darwin19]

you could replace belongs_to :account_status,... by below code
class AccountSerializer < ActiveModel::Serializer
attributes(*Account.attribute_names.map(&:to_sym))
AccountStatus.attribute_names.each do |attr_name|
key = "account_status_#{attr_name}"
define_method(key) do
# i checked and see that `object.account_status` just call one time
object.account_status.send(attr_name)
end
attribute key.to_sym
end
end

Instead of using an assocation in your serializer you can setup delegation in your model:
class Account < ApplicationRecord
delegate :desc, ...,
to: :account_status,
prefix: true
end
This will create a account_status_desc method which you can simply call from your serializer:
class AccountSerializer < ActiveModel::Serializer
attributes(
:foo,
:bar,
:baz,
:account_status_code, # this is already a attribute of Account
:account_status_desc
# ...
)
end
Another way of doing this is by simply adding methods to your serializer:
class AccountSerializer < ActiveModel::Serializer
attributes :foo
def foo
object.bar.do_something
end
end
This is a good alternative when the result of serialization does not align with the internal representation in the model.

Related

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.

Rails 5 "no implicit conversion of Symbol into Integer" for double nested has_many attributes

I keep getting the error no implicit conversion of Symbol into Integer when trying to create/update a Form model via a form. I've narrowed it down to something to do with actions_attributes in the form_params. trigger_attributes works fine if actions_attributes is removed. I suspect it has something to do with the enum field, double nested attributes, and/or the has_many relationship, but not sure.
Any ideas on what could be causing this error?
Running Rails 5.0.x and Ruby 2.3.x, with the relevant models and controller below.
class Form < ApplicationRecord
has_one :rule
accepts_nested_attributes_for :rule
end
class Rule < ApplicationRecord
has_one :trigger
has_many :actions
accepts_nested_attributes_for :trigger
accepts_nested_attributes_for :actions
end
class Trigger < ApplicationRecord
belongs_to :rule
enum name: [:example]
end
class Actions < ApplicationRecord
belongs_to :rule
enum name: [:example]
end
class FormsController < ApplicationController
...
private
def form_params
params.require(:form).permit(
:title,
:description,
rule_attributes: [
trigger_attributes: [:name],
actions_attributes: [:name]
]
)
end
end
I got this to work by changing actions_attributes: to actions: under def form_params, as well as making the relevant change to the form, changing fields_for :actions_attributes to fields_for :actions.
I'm often confused when to use _attributes and when not to. If anyone has information about when to use which, I would appreciate it if you could provide a link to the information in the comments. Thanks.

JSON with full hierarchy in Rails

I have an hierarchical structure in my app:
Environment has Containers
Container has Items
Item has expression
so my Model code looks like:
class Environment < ActiveRecord::Base
has_many :containers, :dependent => :destroy
def as_json(options = {})
super(options.merge(include: :containers))
end
end
class Container < ActiveRecord::Base
has_many :items, :dependent => :destroy
belongs_to :environment
def as_json(options = {})
super(options.merge(include: :items))
end
end
class Item < ActiveRecord::Base
has_many :expressions, :dependent => :destroy
belongs_to :container
def as_json(options = {})
super(options.merge(include: :expressions))
end
end
class Expression < ActiveRecord::Base
belongs_to :item
def as_json(options = {})
super()
end
end
In a regular get of a record I usually need only one hierarchy below the desired record, that's why in the as_json I merge only one hierarchy down (get Environment will return a collection of containers but those containers will not have Items)
My Question:
Now what I need is to add a method to the controller that allows full hierarchy response i.e. GET /environment/getFullHierarchy/3 will return: environment with id=3 with all its containers and for every container all it's Items & for every Item all it's expressions. without breaking the current as_json
I'm kinda new to Rails, wirking with Rails 4.2.6 & don't know where to start - can anyone help?
Sure it goes something like this hopefully you get the idea.
EnvironmentSerializer.new(environment) to get the hierarchy json.
lets say environments table has columns environment_attr1 , environment_attr2
class EnvironmentSerializer < ActiveModel::Serializer
attributes :environment_attr1, :environment_attr2 , :containers
# This method is called if you have defined a
# attribute above which is not a direct value like for
# a rectancle serializer will have attributes length and width
# but you can add a attribute area as a symbol and define a method
# area which returns object.length * object.width
def containers
ActiveModel::ArraySerializer.new(object.containers,
each_serializer: ContainerSerializer)
end
end
class ContainerSerializer < ActiveModel::Serializer
attributes :container_attr1, :container_attr2 , :items
def items
ActiveModel::ArraySerializer.new(object.items,
each_serializer: ItemSerializer)
end
end
class ItemSerializer < ActiveModel::Serializer
...
end
class ExpressionSerializer < ActiveModel::Serializer
...
end

active model serializer with virtual attribute - Rails 4

I am currently making API with RoR, and I need to create an object with virtual attributes and associated object.
The problem is that serializer does not kick in when I return an object with virtual attribute.
Here is the returned object from foo_controller
{
:id=>280,
:virtual=>"y8st07ef7u"
:user_id=>280
}
:virtual is a virtual attribute and user_id is an id of associated table - User.
My goal is to make this
{
:id=>280,
:virtual=>"y8st07ef7u",
:user=>{
:id=>280,
:name=>'foo'
}
}
Foo_controller setting
class Api::V1::FoosController < ApplicationController
foos = Foo.all
foos.each do |foo|
foo.set_attribute('y8st07ef7u')
end
render json: foos.to_json(:methods => :virtual), status: 200
end
Foo_model setting
class Foo < ActiveRecord::Base
belongs_to :user
attr_accessor:virtual
def set_attribute(path)
self.virtual = path
end
end
Foo_serializer setting
class FooSerializer < ActiveModel::Serializer
attributes :id, :virtual
has_one :user
end
Foo migration setting
class CreateFoos < ActiveRecord::Migration
def change
create_table :foo do |t|
t.references :user
end
end
end
user model
class User < ActiveRecord::Base
has_many :foos
end
user serializer
class UserSerializer < ActiveModel::Serializer
attributes :id, :name
belongs_to :foo
end
When I replace "foo.to_json(:methods => :virtual)" in foo_controller with "foos", serializer kicks in and I get a user object inside the returned json instead of user_id, but :virtual is not in the json.
Are there any ways I can get an object with both virtual attributes and associated object using active model serializer.
Thank you in advance for your help!
I figured out. It was very simple.
I just had to add ":virtual" to attributes in the foo_serializer and replace "foo.to_json(:methods =>:virtual)" with just "foos"

how to conditionally include associations in a Rails Active Model Serializer v0.8

I have used AMS (0.8) with Rails 3.2.19 but one place where I really struggle with them is how to control whether serializers include their associations or not. I obviously use AMS to build JSON
Api's. Sometimes a serializer is the leaf or furthest out element and sometimes it's the top level and needs to include associations. My question is what is the best way to do this or is the solution I do below work (or is best solution).
I have seen some of the discussions but I find them very confusing (and version based). It's clear that for Serializer attributes or associations, there is an an include_XXX? method for each and you can return either a truthy or falsey statement here.
Here's my proposed code - it's a winemaker that has many wine_items. Is this how you would do this?
Model Classes:
class WineItem < ActiveRecord::Base
attr_accessible :name, :winemaker_id
belongs_to :winemaker
end
class Winemaker < ActiveRecord::Base
attr_accessible :name
has_many :wine_items
attr_accessor :show_items
end
Serializers:
class WinemakerSerializer < ActiveModel::Serializer
attributes :id, :name
has_many :wine_items
def include_wine_items?
object.show_items
end
end
class WineItemSerializer < ActiveModel::Serializer
attributes :id, :name
end
and in my controller:
class ApiWinemakersController < ApplicationController
def index
#winemakers=Winemaker.all
#winemakers.each { |wm| wm.show_items=true }
render json: #winemakers, each_serializer: WinemakerSerializer, root: "data"
end
end
I ran into this issue myself and this is the cleanest solution so far (but I'm not a fan of it).
This method allows you to do things like:
/parents/1?include_children=true
or using a cleaner syntax like:
/parents/1?include=[children], etc...
# app/controllers/application_controller.rb
class ApplicationController
# Override scope for ActiveModel-Serializer (method defined below)
# See: https://github.com/rails-api/active_model_serializers/tree/0-8-stable#customizing-scope
serialization_scope(:serializer_scope)
private
# Whatever is in this method is accessible in the serializer classes.
# Pass in params for conditional includes.
def serializer_scope
OpenStruct.new(params: params, current_user: current_user)
end
end
# app/serializers/parent_serializer.rb
class ParentSerializer < ActiveModel::Serializer
has_many :children
def include_children?
params[:include_children] == true
# or if using other syntax:
# params[:includes].include?("children")
end
end
Kinda hackish to me, but it works. Hope you find it useful!

Resources