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
Related
I had a model which returns some parameters and includes parameters from other models as follows:
def as_json(options = {})
camelize_keys(super(options.merge(:only => [:id, :userId], include:{
comments: { only: [:test, :id] },
valediction: { only: [:name, :text, :hidden, :order] }
})))
end
def camelize_keys(hash)
values = hash.map do |key, value|
[key.camelize(:lower), value]
end
Hash[values]
end
Now I have moved the code to my controller because different controller actions need to return different parts of the model. (index should just return valediction, but show should return comments and valediction)
The new controller:
def index
respond_with(displayed_user.microposts.all, include: {
valediction: { only: [:name, :text] }
})
end
def show
respond_with(displayed_user.microposts.find(params[:id]), include: {
comments: { only: [:test, :id] },
valediction: { only: [:name, :text, :hidden, :order] }
})
end
But I'm very new to rails and I don't know how to put the camelize_keys function in so that it works.
Doing complex JSON formatting in your controllers / and or models usually leads to bloat and is a pain to test.
A good solution for this is using the ActiveModel::Serializer (AMS) gem. Its included in Rails 5 but you can easily add it to a Rails 4 project by adding it to the gemfile:
# See rubygems.org for latest verstion!
gem 'active_model_serializers', '~> 0.9.3'
Then run bundle install and restart your rails server.
With AMS you create serializer classes which define how your model data should be represented in JSON, XML etc. A serializer is basically a class that takes a model instance (or an array of models) and returns a hash (or an array of hashes) when you call .serializable_hash.
But Rails will take care of that part automatically for you.
class MicropostSerializer < ActiveModel::Serializer
attributes :id, :user_id
has_many :comments
has_many :valedictions
end
class CommentSerializer < ActiveModel::Serializer
attributes :test, :id
end
class ValedictionSerializer < ActiveModel::Serializer
attributes :name, :text, :hidden, :order
end
In your controller you can simply call:
def index
render json: displayed_user.microposts.all
end
But wait, what about camelize_keys?
Unless you have to support some weird legacy client that needs camelized keys there are very few reasons to do this. Most large API's use snakecase (Facebook, Google etc.) and Rails 5 is moving towards the JSONAPI spec which uses snakecase.
From your code sample it seems that some of your rails model attributes (and the db columns backing them) use camelcase. You should change the DB column with a migration as soon as possible.
If you HAVE to support a legacy database you can use alias_attribute:
class Pet < ActiveRecord::Base
alias_attribute :ownerId, :owner_id
end
You could move the method to a class method in the model, eg
#class methods
class << self
def camelize_keys(hash)
values = hash.map do |key, value|
[key.camelize(:lower), value]
end
Hash[values]
end
end
Now you can call this from anywhere like
MyModel.camelize_keys(some_hash)
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
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
I am using ActiveModel Serializers in a Rails project.
The default serializer for the object is fairly large, and nesting an object in API responses result in rather large JSON objects.
Sometimes, I want to embed an object, but only need a small subset of the object's attributes to be present in the JSON.
Obviously, I could do something like this:
render json: #user, serializer: SmallerUserSerializer
but that would lead to a lot of duplication.
Is there an option that I can pass to the serializer so that it will only include a subset of the serializers attributes? Eg:
class BlogSerializer
# This is pseudocode. Does not actually work.
has_one :user, only_show: [:user_id, :profile_url]
end
Create a method and call to_json on the user object. Then add that method name to your list of attributes. The method can be called user also.
class BlogSerializer
attributes :id, :user
def user
object.user.to_json( only: [ :id, :profile_url ] )
end
end
Use the active model serialzers gem.
Your pseudo code will become the following simple modularized code:
class BlogSerializer < ActiveModel::Serializer
attributes :user_id, :profile_url
end
Guide: http://railscasts.com/episodes/409-active-model-serializers
Create a method and call to_json on the user object. Then add that method name to your list of attributes. The method can be called user also.
class BlogSerializer
require 'json'
attributes :id, :user
def user
JSON.parse "#{object.user.to_json( only: [ :id, :profile_url ] )}"
end
end
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.