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"
Related
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.
I have three serializers, nested within each other. Like this:
class PersonSerializer < ActiveModel::Serializer
attributes :id :name
has_many: companies
class Company < ActiveModel::Serializer
has_many :products
class ProductSerializer < ActiveModel::Serializer
has_many :product_items do
unless person.id != object.company.user_id
object.product_items
end
end
end
end
end
My issue is the line: unless person.id != object.company.user_id.
person is undefined here. How do I get access to the current person instance within the ProductSerializer?
I am unsure why would you need that structure, but general class definition declares a new scope. Whether you need to capture variables from the parent scope, use closure with Class#new instead:
class PersonSerializer < ActiveModel::Serializer
attributes :id :name
has_many: companies
Company = Class.new(ActiveModel::Serializer) do
has_many :products
ProductSerializer = Class.new(ActiveModel::Serializer) do
has_many :product_items do
unless person.id != object.company.user_id
object.product_items
end
end
end
end
end
I am trying to create a person object in my application but within the controller when #person.save! is called the exception ActiveRecord::RecordInvalid: Validation failed: Contacts contactable can't be blank is thrown. This is due to the contactable_id not being set.
I know that I can save the person and then create and save a contact, but would preder to save all in one hit. What am I missing?
The classes involved are below:
person.rb
class Person < ActiveRecord::Base
has_many :contacts, as: :contactable
accepts_nested_attributes_for :contacts
end
contact.rb
class Contact < ActiveRecord::Base
belongs_to :contactable, polymorphic: true
end
The form posts the following:
{"commit"=>"Add Person",
"person"=>{"name"=>"Fred Bloggs"},
"website"=>"http://www.example.com",
"controller"=>"person",
"action"=>"create"}
and is processed in the controller create method.
person_controller.rb
class PersonController < ApplicationController
def create
load = person_params.merge({contacts_attributes: [{address:params[:website],contact_type:"web", contact_sub_type: "main"}]})
#person = Person.new(load)
#person.save!
end
private
def person_params
params.require(:person).permit( ..... contacts_attributes: [:address, :contact_type, :contact_sub_type])
end
end
How can I specify that I want to serialize additional attributes of classes that inherit from an abstract one ?
(I am using ActiveModel::Serializer)
EDIT : I have associations/references that I need to keep in my serializer, not just attributes
class AbstractClass
[attributes]
**has_one :reference** # Added after Edit
end
class Foo < AbstractClass
field :some_special_foo_field
end
class Bar < AbstractClass
field :some_other_bar_field
end
Class Baz
has_many :abstract_class
end
I want a JSON that would look like
{:baz => [
{ id => "foo_1_ID",
some_special_foo_field => "something"
}, {
id => "bar_1_ID,
some_other_bar_field => "Somewhere"
}]
Somewhere in my controller I am doing:
#bazz = Baz.all
respond_to do |format|
format.json { render json: #bazz}
end
class BazSerializer < ActiveModel::Serializer
attributes [what I need]
has_many :constraints, serializer: AbstractClassShortSerializer, embed: :objects
end
class AbstractClassSerializer
**has_one :reference, serializer: SomethingSerializer, embed: :objects** # Added after Edit
attributes :[attributes I want to keep]
end
Now Problem : depending on whether the objects in #bazz are Foo or Bar, I'd like to have their special attributes in my JSON, but I currently can't with this code
This question is somewhat linked to this one Simulating constraints and sub-constraints
Make it simple:
do one serializer for foos and one serializer for bars
dont use has_many in your baz serializer
Do this way:
class AbstractClassShortSerializer < ActiveModel::Serializer
attributes common_attributes_here
end
class BarSerializer < AbstractClassShortSerializer
end
class BazSerializer < ActiveModel::Serializer
attributes [what I need]
attributes :constraints
def constraints
object.constraints.map do |constraint|
if constraint.foo?
FooSerializer
elsif constraint.baz?
BazSerializer
else
AbstractClassShortSerializer
end.new(constraint, scope: scope).attributes
end
end
end
Using Rails 3.2, I have the following:
# shop.rb
class Shop < ActiveRecord::Base
has_many :nearby_shops
after_update :find_nearby_shops
def find_nearby_shops
NearbyShop.create(
:shop_id => self.id,
:shop_type => "test",
)
end
end
# nearby_shop.rb
class NearbyShop < ActiveRecord::Base
attr_accessible :shop_id, :shop_type
belongs_to :shop
end
I find it was necessary to declare shop_id in attr_accessible to get the shop_id saved, else it would be blank. Is this behavior correct?
Yes, it is. Otherwise you could use
def find_nearby_shops
self.nearby_shops.create(
shop_type: "test"
)
end
self.nearby_shops will scope to the nearby_shops with current shop it and bypasses the protected attributes because you are not using a mass assignment method (such as create) to assign the shop_id.