I have a simple model:
class Receipt
include ActiveModel::Serialization
attr_accessor :products
end
and my controller is doing:
def create
respond_with receipt, :serializer => ReceiptSerializer
end
and the serializer:
class ReceiptSerializer < ActiveModel::Serializer
attributes :products
end
and I get:
NoMethodError:
undefined method `to_model' for #<Receipt:0x007f99bcb3b6d8>
Yet if I change my controller to:
def create
json = ReceiptSerializer.new(receipt)
render :json => json
end
Then everything works fine... what is happening???
I was using active_model_serializers 0.9.3, but just tried 0.10.2, and the results are the same.
In all the documentation I've read and personal implementation I use render json: instead of respond_with.
render json: receipt, serializer: ReceiptSerializer
I believe that respond_with has been removed from rails and isn't considered a best practice anymore but I can't find a link to validate that claim.
I'm not totally sure, but it seems in your Receipt PORO, you should rather include: ActiveModel::SerializerSupport.
I can't confirm if that works for active_model_serializers 0.10.2 though
Related
I'm trying to get my Rails API to render all JSON responses in camelCase. Currently I am using Netflix Fast JSON API for my serializer and rendering errors like so:
render json: { errors: command.errors }, status: :unauthorized
For the Netflix Fast JSON API serializers I've been adding set_key_transform :camel_lower to every serializer, which seems to do the trick (although if anyone knows how to make that a default it would be much appreciated).
For rendering errors however I'm not sure the best way to go about camel casing. If anyone has any experience with this please let me know how you go about it! Ideally there is a method of doing this that doesn't add too much syntax to every render call being made.
UPDATE
In serializing errors I added a helper method on the application controller:
def render_error(errors_params, status)
render json: {
errors: errors_params
}.deep_transform_keys { |key| key.to_s.camelize(:lower) }, status: status
end
For the Netflix Fast JSON API I took the suggestion of #spickermann and added an application serializer for the other serializers to inherit from:
class ApplicationSerializer
include FastJsonapi::ObjectSerializer
set_key_transform :camel_lower
end
class SomeSerializer < ApplicationSerializer
attributes :attribute, :other_attribute
end
You could create an ApplicationSerializer and all other serializers could inherit from it:
class ApplicationSerializer
include FastJsonapi::ObjectSerializer
set_key_transform :camel_lower
end
class FooBarSerializer < ApplicationSerializer
attributes :buzz, :fizz
# ...
end
You could monkey patch the serializer
Rails.application.config.to_prepare do
FastJsonapi::ObjectSerializer.class_eval do
set_key_transform :camel_lower
end
end
and for handling errors you can probably create an error serializer
render serializer: ErrorSerializer, json: {status: : unauthorized, errors: resource.errors
Have a look here and here
EDIT: TLDR: This boils down to serializing the attachments. See my response.
I can see two ways to achieve this:
(1) Serialize the attachments (with id and url attributes), thus providing an id to the FE that they can use to DELETE /attachments/:id which would then call ActiveStorage::Attachment.find(:id).purge. The problem is with serializing as attachments do not have built-in models. I tried creating an ActiveStorageAttachment model for the active_storage_attachments table but could not get the url for the attachment as the Rails.application.routes.url_helpers.url_for(#object) requires an ActiveStorage::Attachment object not an ActiveStorageAttachment object.
(2) Another option would be to have a DELETE /attachments/:attachment_url endpoint. For this to work, I'd need to get the ActiveStorage::Attachment object based on the url, in order to call purge on it. Not sure if that is possible?
I'd much prefer the first solution, it feels cleaner and more adaptable. Any help with either approach would be much appreciated!
In the end, I managed to serialize the attached images (option(1) above) using the jsonapi-rb gem.
In the controller, I include: :images and pass in the attachment type using the expose option:
class PostsController < ApplicationController
...
def show
respond_to do |format|
format.html
format.json { render jsonapi: #post, include: :images, expose: {attachment_type: "images"} }
end
end
end
ActiveSupport gives you the post.images_attachments method for free:
class SerializablePost < JSONAPI::Serializable::Resource
type 'posts'
...
has_many :images do
#object.images_attachments
end
end
I created an attachment serializer:
class SerializableAttachment < JSONAPI::Serializable::Resource
include Rails.application.routes.url_helpers
type do
#attachment_type
end
attribute :id
attribute :url do
url_for(#object)
end
end
I needed to tell jsonapi to use this serializer for all attachments:
class ApplicationController < ActionController::Base
...
def jsonapi_class
super.merge(
'ActiveStorage::Attachment': SerializableAttachment
)
end
end
Now I can implement DELETE /attachments/:id.
In Topic model:
class Topic < ActiveRecord::Base
has_many :choices, :dependent => :destroy
accepts_nested_attributes_for :choices
attr_accessible :title, :choices
end
During a POST create, the params submitted is :choices, instead of :choices_attributes expected by Rails, and giving an error:
ActiveRecord::AssociationTypeMismatch (Choice(#70365943501680) expected,
got ActiveSupport::HashWithIndifferentAccess(#70365951899600)):
Is there a way to config accepts_nested_attributes_for to accept params passing as choices instead of choices_attributes in a JSON call?
Currently, I did the attributes creation in the controller (which seems not to be an elegant solution):
def create
choices = params[:topic].delete(:choices)
#topic = Topic.new(params[:topic])
if choices
choices.each do |choice|
#topic.choices.build(choice)
end
end
if #topic.save
render json: #topic, status: :created, location: #topic
else
render json: #topic.errors, status: :unprocessable_entity
end
end
This is an older question, but I just ran into the same problem. Is there any other way around this? It looks like that "_attributes" string is hardcoded in the nested_attributes.rb code (https://github.com/rails/rails/blob/master/activerecord/lib/active_record/nested_attributes.rb#L337).
Assigning "choices_attributes" to a property when submitting a form is fine, but what if it's being used for an API. In that case it just doesn't make sense.
Does anyone have a way around this or an alternative when passing JSON for an API?
Thanks.
UPDATE:
Well, since I haven't heard any updates on this I'm going to show how I'm getting around this right now. Being new to Rails, I'm open to suggestions, but this is the only way I can figure it out at the moment.
I created an adjust_for_nested_attributes method in my API base_controller.rb
def adjust_for_nested_attributes(attrs)
Array(attrs).each do |param|
if params[param].present?
params["#{param}_attributes"] = params[param]
params.delete(param)
end
end
end
This method basically converts any attributes that are passed in to #{attr}_attributes so that it works with accepts_nested_attributes_for.
Then in each controller that needs this functionality I added a before_action like so
before_action only: [:create] do
adjust_for_nested_attributes(:choices)
end
Right now I'm only worried about creation, but if you needed it for update you could add that into the 'only' clause of the before_action.
You can create method choices= in model as
def choices=(params)
self.choices_attributes = params
end
But you'll break your setter for choices association.
The best way is to modify your form to return choices_attributes instead choices
# Adds support for creating choices associations via `choices=value`
# This is in addition to `choices_attributes=value` method provided by
# `accepts_nested_attributes_for :choices`
def choices=(value)
value.is_a?(Array) && value.first.is_a?(Hash) ? (self.choices_attributes = value) : super
end
My setup: Rails 2.3.10, Ruby 1.8.7
I have experimented, without success, with trying to access a virtual attribute in a model from a JSON call. Let's say I have the following models and controller code
class Product
name,
description,
price,
attr_accessor :discounted_price
end
class Price
discount
end
class ProductsController
def show
#product = Product.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.json { render :json => #product }
end
end
end
What I like is to have the JSON output also include Product.discounted_price which is calculated in real-time for each call, ie discounted_price = Price.discount * Product.price. Is there a way to accomplish this?
SOLUTION:
With the initial help from dmarkow, I figured it out, my actual scenario is more complex than the above example. I can do something like this, in the Product model, add a getter method
def discounted_price
...# do the calculation here
end
In the JSON call do this
store = Store.find(1)
store.as_json(:include => :products, :methods => :discounted_price)
You can run to_json with a :methods parameter to include the result of those method(s).
render :json => #product.to_json(:methods => :discounted_price)
Have a look at the gem RABL, as shown in this railscast:
http://railscasts.com/episodes/322-rabl?view=asciicast
RABL gives you fine grained control of the json you produce, including collections and children.
I am having a problem in RSpec when my mock object is asked for a URL by the ActionController. The URL is a Mock one and not a correct resource URL.
I am running RSpec 1.3.0 and Rails 2.3.5
Basically I have two models. Where a subject has many notes.
class Subject < ActiveRecord::Base
validates_presence_of :title
has_many :notes
end
class Note < ActiveRecord::Base
validates_presence_of :title
belongs_to :subject
end
My routes.rb file nests these two resources as such:
ActionController::Routing::Routes.draw do |map|
map.resources :subjects, :has_many => :notes
end
The NotesController.rb file looks like this:
class NotesController < ApplicationController
# POST /notes
# POST /notes.xml
def create
#subject = Subject.find(params[:subject_id])
#note = #subject.notes.create!(params[:note])
respond_to do |format|
format.html { redirect_to(#subject) }
end
end
end
Finally this is my RSpec spec which should simply post my mocked objects to the NotesController and be executed... which it does:
it "should create note and redirect to subject without javascript" do
# usual rails controller test setup here
subject = mock(Subject)
Subject.stub(:find).and_return(subject)
notes_proxy = mock('association proxy', { "create!" => Note.new })
subject.stub(:notes).and_return(notes_proxy)
post :create, :subject_id => subject, :note => { :title => 'note title', :body => 'note body' }
end
The problem is that when the RSpec post method is called.
The NotesController correctly handles the Mock Subject object, and create! the new Note object. However when the NoteController#Create method tries to redirect_to I get the following error:
NoMethodError in 'NotesController should create note and redirect to subject without javascript'
undefined method `spec_mocks_mock_url' for #<NotesController:0x1034495b8>
Now this is caused by a bit of Rails trickery that passes an ActiveRecord object (#subject, in our case, which isn't ActiveRecord but a Mock object), eventually to url_for who passes all the options to the Rails' Routing, which then determines the URL.
My question is how can I mock Subject so that the correct options are passed so that I my test passes.
I've tried passing in :controller => 'subjects' options but no joy.
Is there some other way of doing this?
Thanks...
Have a look at mock_model, which is added by rspec-rails to make it easier to mock ActiveRecord objects. According to the api docs:
mock_model: Creates a mock object instance for a model_class with common methods stubbed out.
I'm not sure if it takes care of url_for, but it's worth a try.
Update, 2018-06-05:
As of rspec 3:
mock_model and stub_model have been extracted into the rspec-activemodel-mocks gem.
In case zetetic's idea doesn't work out, you can always say Subject.new and then stub out to_param and whatever else you might need faked for your example.