active model serializer not working with rails-api gem - ruby-on-rails

I am using rails-api gem in my project for json api, and for that purpose I used active model serializer gem for serializing my objects but some how the objects are not being serialized using active model serializer.
I have a MessageSerializer inside of my serializers folder
class MessageSerializer < ActiveModel::Serializer
attributes :id, :sender_id, :recipient_id, :sender_type, :subj, :body, :status, :sender
def sender
object.user.try('username')
end
end
And my messages controller is as follows
class Api::MessagesController < Api::BaseController
def index
#messages = current_user.incoming_messages
render json: #messages, serializer: MessageSerializer
end
end
But the problem is that the serialized object thrown to client contains all the fields in message model ie; it contains created_at, updated_at fields too.
Seems like its not using serializer.
What might have gone wrong?
I searched a lot about it but didn't found any post that helped me.
Thanks

In your BaseController, did you add the include bellow ?
include ActionController::Serialization

What version of AMS are you using?
I had this same issue and was able to fix it by changing the AMS version from 0.9.X to 0.8.X. This can be done by adding a version number to your Gemfile.
gem 'active_model_serializers', '~> 0.8.0'
There are notes about this on the AMS GitHub repo.
https://github.com/rails-api/active_model_serializers#maintenance-please-read

That's because the serialization is not loaded by default in rails-api.
You have to do this:
class ApplicationController < ActionController::API
include ::ActionController::Serialization
end

I didn't downgrade, I spent some time trying different things and at the end I get to a pattern like this:
def sender
if object.sender
serializer = SenderSerializer.new(object.sender)
ActiveModel::Serializer::Adapter::JsonApi.new(serializer).as_json[:senders]
end
end
it's ugly but it did the trick for me.
For a has_many relation, you can do something like this:
def attachments
attachments = object.attachments.to_a
return [] if attachments.empty?
serializer = ActiveModel::Serializer::ArraySerializer.new(attachments, each_serializer:AttachmentSerializer)
ActiveModel::Serializer::Adapter::JsonApi.new(serializer).as_json[:attachments]
end

Related

How to return all attributes of an object with Rails Serializer?

I have a simple question. I have a seriaizer that looks like this:
class GroupSerializer < ActiveModel::Serializer
attributes :id, :name, :about, :city
end
The problem is that, whenever I change my model, I have to add/remove attributes from this serializer. I just want to get the whole object by default as in the default rails json respond:
render json: #group
How can I do that?
At least on 0.8.2 of ActiveModelSerializers you can use the following:
class GroupSerializer < ActiveModel::Serializer
def attributes
object.attributes.symbolize_keys
end
end
Be carful with this though as it will add every attribute that your object has attached to it. You probably will want to put in some filtering logic on your serializer to prevent sensitive information from being shown (i.e., encrypted passwords, etc...)
This does not address associations, although with a little digging around you could probably implement something similar.
============================================================
UPDATE: 01/12/2016
On 0.10.x version of ActiveModelSerializers, attributes receives two arguments by default. I added *args to avoid exception:
class GroupSerializer < ActiveModel::Serializer
def attributes(*args)
object.attributes.symbolize_keys
end
end
Just to add to #kevin's answer. I was looking also to how to add filters on the returned attributes. I looked to the the documentation active_model_serializers 0.9 and it does support filters that looks like this:
def attributes
object.attributes.symbolize_keys
end
def filter(keys)
keys - [:author, :id]
end
I tried it, but it did not work. I assumed that's because the attributes are not specified explicitly. I had to do it the same way specified in the rails cast to work:
##except=[:author, :id]
def attributes
data = object.attributes.symbolize_keys
##except.each { |e| data.delete e }
data
end
Try the following to get all the attribute keys for the Group class:
Group.new.attributes.keys
For example, I get the following for users on one app:
> User.new.attributes.keys
=> ["id", "password_digest", "auth_token", "password_reset_token", "password_reset_requested_at", "created_at", "updated_at"]
On 0.10.x version of ActiveModelSerializers, attributes receives two arguments by default. I added *args to avoid exception:
class GroupSerializer < ActiveModel::Serializer
def attributes(*args)
object.attributes.symbolize_keys
end
end
I want get all attributes + few more.
base on answer above, this work:
class NotificationSerializer < ActiveModel::Serializer
def actor
'asdasd'
end
def attributes(*args)
keys = object.attributes
keys[:actor] = actor() # add attribute here
keys.symbolize_keys
end
end

Elasticsearch Tire Carrierwave remote upload

I'm desperately don't success to make Carrierwave working with Tire (Elasticsearch gem).
I have a Question model which was an ActiveRecord one, but I migrated it on Elasticsearch with Tire. Until it was on ActiveRecord everything was working great. But not anymore.
What I want is to upload a remote file (from Facebook) to a S3 bucket. All config files are correct (as it was working under ActiveRecord model)
Here is my Question model:
class Question
include ActiveModel::MassAssignmentSecurity
include ActiveModel::Validations
extend CarrierWave::Mount
include Tire::Model::Callbacks
include Tire::Model::Persistence
# set fields for carrierwave uploader
mount_uploader :path, QuestionUploader
validates_presence_of :question
attr_accessible :path
attr_accessor :remote_path_url, :remove_path
property :difficulty
property :question
property :path
end
And then in my questions_controller:
class QuestionsController < ApplicationController
def create
#question = Question.new question: "How are you ?", difficulty: 3
#question.remote_path_url = "http://domain.com/file.jpg"
#question.save
render nothing: true
end
end
Elasticsearch record works, but no upload happens...
Someone has an idea ?
Cheers
After save callback should be explicitly called for non active record models, as mentioned here.
Like #question.store_image!
Also it may not be good idea to use path as file name, as it is confusing.
For example: #question.store_path!
I had a similar problem and ended up using Dragonfly for Models which integrated very well with a non-ActiveRecord model. I am using elasticsearch-persistence with a standalone persistence layer for models.

Rails 4 Not Updating Nested Attributes Via JSON

I've scoured related questions and still have a problem updating nested attributes in rails 4 through JSON returned from my AngularJS front-end.
Question: The code below outlines JSON passed from AngularJS to the Candidate model in my Rails4 app. The Candidate model has many Works, and I'm trying to update the Works model through the Candidate model. For some reason the Works model fails to update, and I'm hoping someone can point out what I'm missing. Thanks for your help.
Here's the json in the AngularJS front-end for the candidate:
{"id"=>"13", "nickname"=>"New Candidate", "works_attributes"=>[
{"title"=>"Financial Analyst", "description"=>"I did things"},
{"title"=>"Accountant", "description"=>"I did more things"}]}
Rails then translates this JSON into the following by adding the candidate header, but does not include the nested attributes under the candidate header and fails to update the works_attributes through the candidate model:
{"id"=>"13", "nickname"=>"New Candidate", "works_attributes"=>[
{"title"=>"Financial Analyst", "description"=>"I did things"},
{"title"=>"Accountant", "description"=>"I did more things"}],
"candidate"=>{"id"=>"13", "nickname"=>"New Candidate"}}
The candidate_controller.rb contains a simple update:
class CandidatesController < ApplicationController
before_filter :authenticate_user!
respond_to :json
def update
respond_with Candidate.update(params[:id], candidate_params)
end
private
def candidate_params
params.require(:candidate).permit(:nickname,
works_attributes: [:id, :title, :description])
end
end
The candidate.rb model includes the following code defining the has_many relationship with the works model:
class Candidate < ActiveRecord::Base
## Model Relationships
belongs_to :users
has_many :works, :dependent => :destroy
## Nested model attributes
accepts_nested_attributes_for :works, allow_destroy: true
## Validations
validates_presence_of :nickname
validates_uniqueness_of :user_id
end
And finally, the works.rb model defines the other side of the has_many relationship:
class Work < ActiveRecord::Base
belongs_to :candidate
end
I appreciate any help you may be able to provide as I'm sure that I'm missing something rather simple.
Thanks!
I've also been working with a JSON API between Rails and AngularJS. I used the same solution as RTPnomad, but found a way to not have to hardcode the include attributes:
class CandidatesController < ApplicationController
respond_to :json
nested_attributes_names = Candidate.nested_attributes_options.keys.map do |key|
key.to_s.concat('_attributes').to_sym
end
wrap_parameters include: Candidate.attribute_names + nested_attributes_names,
format: :json
# ...
end
Refer to this issue in Rails to see if/when they fix this problem.
Update 10/17
Pending a PR merge here: rails/rails#19254.
I figured out one way to resolve my issue based on the rails documentation at: http://edgeapi.rubyonrails.org/classes/ActionController/ParamsWrapper.html
Basically, Rails ParamsWrapper is enabled by default to wrap JSON from the front-end with a root element for consumption in Rails since AngularJS does not return data in a root wrapped element. The above documentation contains the following:
"On ActiveRecord models with no :include or :exclude option set, it will only wrap the parameters returned by the class method attribute_names."
Which means that I must explicitly include nested attributes with the following statement to ensure Rails includes all of the elements:
class CandidatesController < ApplicationController
before_filter :authenticate_user!
respond_to :json
wrap_parameters include: [:id, :nickname, :works_attributes]
...
Please add another answer to this question if there is a better way to pass JSON data between AngularJS and Rails
You can also monkey patch parameter wrapping to always include nested_attributes by putting this into eg wrap_parameters.rb initializer:
module ActionController
module ParamsWrapper
Options.class_eval do
def include
return super if #include_set
m = model
synchronize do
return super if #include_set
#include_set = true
unless super || exclude
if m.respond_to?(:attribute_names) && m.attribute_names.any?
self.include = m.attribute_names + nested_attributes_names_array_of(m)
end
end
end
end
private
# added method. by default code was equivalent to this equaling to []
def nested_attributes_names_array_of model
model.nested_attributes_options.keys.map { |nested_attribute_name|
nested_attribute_name.to_s + '_attributes'
}
end
end
end
end

How is attr_accessible used in Rails 4?

attr_accessible seems to no longer work within my model.
What is the way to allow mass assignment in Rails 4?
Rails 4 now uses strong parameters.
Protecting attributes is now done in the controller. This is an example:
class PeopleController < ApplicationController
def create
Person.create(person_params)
end
private
def person_params
params.require(:person).permit(:name, :age)
end
end
No need to set attr_accessible in the model anymore.
Dealing with accepts_nested_attributes_for
In order to use accepts_nested_attribute_for with strong parameters, you will need to specify which nested attributes should be whitelisted.
class Person
has_many :pets
accepts_nested_attributes_for :pets
end
class PeopleController < ApplicationController
def create
Person.create(person_params)
end
# ...
private
def person_params
params.require(:person).permit(:name, :age, pets_attributes: [:name, :category])
end
end
Keywords are self-explanatory, but just in case, you can find more information about strong parameters in the Rails Action Controller guide.
Note: If you still want to use attr_accessible, you need to add protected_attributes to your Gemfile. Otherwise, you will be faced with a RuntimeError.
If you prefer attr_accessible, you could use it in Rails 4 too.
You should install it like gem:
gem 'protected_attributes'
after that you could use attr_accessible in you models like in Rails 3
Also, and i think that is the best way- using form objects for dealing with mass assignment, and saving nested objects, and you can also use protected_attributes gem that way
class NestedForm
include ActiveModel::MassAssignmentSecurity
attr_accessible :name,
:telephone, as: :create_params
def create_objects(params)
SomeModel.new(sanitized_params(params, :create_params))
end
end
An update for Rails 5:
gem 'protected_attributes'
doesn't seem to work anymore. But give:
gem 'protected_attributes_continued'
a try.
We can use
params.require(:person).permit(:name, :age)
where person is Model, you can pass this code on a method person_params & use in place of params[:person] in create method or else method
1) Update Devise so that it can handle Rails 4.0 by adding this line to your application's Gemfile:
gem 'devise', '3.0.0.rc'
Then execute:
$ bundle
2) Add the old functionality of attr_accessible again to rails 4.0
Try to use attr_accessible and don't comment this out.
Add this line to your application's Gemfile:
gem 'protected_attributes'
Then execute:
$ bundle
I had to migrate a Rails app from 3.2 to 6.1 so even gem 'protected_attributes' was not an option. I appreciate the arguments for using require().permit() in the controller, but I didn't want to retype or cut and paste all those attributes from the models, so I decided instead to use this initializer code (put in a file in config/initializers):
# fix attr_accessible in an initializer
# wrap ActionController::Parameters code in singleton method defined
# from attr_accessible so controller code can call class method
# to get permitted parameter list
# e.g. model: class A < ActiveRecord::Base,
# controller calls A.permit_attr(params)
# lots simpler than moving all attr_accessible definitions to controllers
# bug: fails if more than one attr_accessible statement
def (ActiveRecord::Base).attr_accessible *fields
puts "attr_accessible:"+self.name+":permitted_params fields=#{fields.inspect}"
define_singleton_method("permit_attr") { |params|
# may have subclasses where attr_accessible is in superclass
# thus must require by subclass name so should calculate require at runtime
rq = self.name.downcase.to_sym
puts "...permit_attr:self=#{rq} permit(#{fields.inspect})"
params.require(rq).permit(fields)
}
end
To protect against multiple attr_accessible declarations, before defining the method, add
raise "error: model can only have one attr_accessible declaration" if defined? permit_attr

ActiveRecord serialize using JSON instead of YAML

I have a model that uses a serialized column:
class Form < ActiveRecord::Base
serialize :options, Hash
end
Is there a way to make this serialization use JSON instead of YAML?
In Rails 3.1 you can just
class Form < ActiveRecord::Base
serialize :column, JSON
end
In Rails 3.1 you can use custom coders with serialize.
class ColorCoder
# Called to deserialize data to ruby object.
def load(data)
end
# Called to convert from ruby object to serialized data.
def dump(obj)
end
end
class Fruits < ActiveRecord::Base
serialize :color, ColorCoder.new
end
Hope this helps.
References:
Definition of serialize:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/base.rb#L556
The default YAML coder that ships with rails:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/coders/yaml_column.rb
And this is where the call to the load happens:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/attribute_methods/read.rb#L132
Update
See mid's high rated answer below for a much more appropriate Rails >= 3.1 answer. This is a great answer for Rails < 3.1.
Probably this is what you're looking for.
Form.find(:first).to_json
Update
1) Install 'json' gem:
gem install json
2) Create JsonWrapper class
# lib/json_wrapper.rb
require 'json'
class JsonWrapper
def initialize(attribute)
#attribute = attribute.to_s
end
def before_save(record)
record.send("#{#attribute}=", JsonWrapper.encrypt(record.send("#{#attribute}")))
end
def after_save(record)
record.send("#{#attribute}=", JsonWrapper.decrypt(record.send("#{#attribute}")))
end
def self.encrypt(value)
value.to_json
end
def self.decrypt(value)
JSON.parse(value) rescue value
end
end
3) Add model callbacks:
#app/models/user.rb
class User < ActiveRecord::Base
before_save JsonWrapper.new( :name )
after_save JsonWrapper.new( :name )
def after_find
self.name = JsonWrapper.decrypt self.name
end
end
4) Test it!
User.create :name => {"a"=>"b", "c"=>["d", "e"]}
PS:
It's not quite DRY, but I did my best. If anyone can fix after_find in User model, it'll be great.
My requirements didn't need a lot of code re-use at this stage, so my distilled code is a variation on the above answer:
require "json/ext"
before_save :json_serialize
after_save :json_deserialize
def json_serialize
self.options = self.options.to_json
end
def json_deserialize
self.options = JSON.parse(options)
end
def after_find
json_deserialize
end
Cheers, quite easy in the end!
The serialize :attr, JSON using composed_of method works like this:
composed_of :auth,
:class_name => 'ActiveSupport::JSON',
:mapping => %w(url to_json),
:constructor => Proc.new { |url| ActiveSupport::JSON.decode(url) }
where url is the attribute to be serialized using json
and auth is the new method available on your model that saves its value in json format to the url attribute. (not fully tested yet but seems to be working)
I wrote my own YAML coder, that takes a default. Here is the class:
class JSONColumn
def initialize(default={})
#default = default
end
# this might be the database default and we should plan for empty strings or nils
def load(s)
s.present? ? JSON.load(s) : #default.clone
end
# this should only be nil or an object that serializes to JSON (like a hash or array)
def dump(o)
JSON.dump(o || #default)
end
end
Since load and dump are instance methods it requires an instance to be passed as the second argument to serialize in the model definition. Here's an example of it:
class Person < ActiveRecord::Base
validate :name, :pets, :presence => true
serialize :pets, JSONColumn.new([])
end
I tried creating a new instance, loading an instance, and dumping an instance in IRB, and it all seemed to work properly. I wrote a blog post about it, too.
A simpler solution is to use composed_of as described in this blog post by Michael Rykov. I like this solution because it requires the use of fewer callbacks.
Here is the gist of it:
composed_of :settings, :class_name => 'Settings', :mapping => %w(settings to_json),
:constructor => Settings.method(:from_json),
:converter => Settings.method(:from_json)
after_validation do |u|
u.settings = u.settings if u.settings.dirty? # Force to serialize
end
Aleran, have you used this method with Rails 3? I've somewhat got the same issue and I was heading towards serialized when I ran into this post by Michael Rykov, but commenting on his blog is not possible, or at least on that post. To my understanding he is saying that you do not need to define Settings class, however when I try this it keeps telling me that Setting is not defined. So I was just wondering if you have used it and what more should have been described? Thanks.

Resources